Loading learning content...
Garbage-collected languages like Java, C#, Python, and Kotlin have freed developers from manual memory management—a massive win for productivity and safety. But this freedom comes with a subtle trap: garbage collection only handles memory, not other resources.
File handles, database connections, network sockets, and locks must still be released at predictable times. Unlike memory, these resources are finite, expensive, and often represent communication with external systems that expect timely cleanup.
This page explores the language constructs that bring RAII-like deterministic cleanup to garbage-collected languages: Java's try-with-resources, C#'s using statement, Python's context managers, and Kotlin's use function. These patterns ensure that resources are released exactly when expected, regardless of exceptions or complex control flow.
By the end of this page, you will master the try-with-resources pattern across multiple languages, understand the underlying interfaces and protocols, handle edge cases like multiple resources and exception suppression, and know how to create your own resource-managing types compatible with these patterns.
Before garbage-collected languages adopted deterministic cleanup patterns, developers relied on finalizers—special methods called by the garbage collector when an object is about to be destroyed. This approach has severe problems that make it unsuitable for resource management.
12345678910111213141516171819202122232425262728293031323334353637
// DON'T DO THIS: Using finalize() for resource cleanuppublic class DatabaseConnection { private Connection connection; public DatabaseConnection(String url) throws SQLException { connection = DriverManager.getConnection(url); } // DANGEROUS: finalize() is NOT reliable for cleanup @Override protected void finalize() throws Throwable { try { if (connection != null && !connection.isClosed()) { connection.close(); // This might run hours later, or never! } } finally { super.finalize(); } }} // What happens in practice:void problematicUsage() throws SQLException { for (int i = 0; i < 10000; i++) { DatabaseConnection db = new DatabaseConnection(url); db.query("SELECT 1"); // Lost reference - but connection not closed! // GC might not run for a long time // Database connection pool exhausted after ~100 iterations } // System.gc() doesn't guarantee finalization either!} // Real-world consequence:// "Too many connections" error after running for a few minutes// Production incident at 2 AM when traffic spike exhausts connection poolJava's finalize() method has been deprecated since Java 9 and is marked for removal in future versions. C#'s finalizers (destructors) should only be used with the IDisposable pattern as a backup. Modern code should never rely on finalizers for deterministic cleanup. Use try-with-resources or using statements instead.
Introduced in Java 7, the try-with-resources statement provides RAII-like semantics for Java. Any object implementing the AutoCloseable interface can be used in a try-with-resources block, and its close() method will be called automatically when the block exits—whether normally, through an exception, or via return.
The AutoCloseable interface:
public interface AutoCloseable {
void close() throws Exception;
}
The related Closeable interface (extending AutoCloseable) is for I/O resources and declares throws IOException specifically.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
import java.io.*;import java.sql.*; // Basic try-with-resources usagepublic void readFile(String path) throws IOException { // Resource declared in try() - automatically closed try (BufferedReader reader = new BufferedReader(new FileReader(path))) { String line; while ((line = reader.readLine()) != null) { processLine(line); } } // reader.close() called here - GUARANTEED} // Multiple resources - closed in reverse order of declarationpublic void copyFile(String source, String dest) throws IOException { try ( FileInputStream in = new FileInputStream(source); FileOutputStream out = new FileOutputStream(dest) ) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } // out.close() called first, then in.close()} // Database transaction with automatic rollbackpublic void transferFunds(String fromId, String toId, BigDecimal amount) throws SQLException { try ( Connection conn = dataSource.getConnection(); PreparedStatement debit = conn.prepareStatement( "UPDATE accounts SET balance = balance - ? WHERE id = ?"); PreparedStatement credit = conn.prepareStatement( "UPDATE accounts SET balance = balance + ? WHERE id = ?") ) { conn.setAutoCommit(false); debit.setBigDecimal(1, amount); debit.setString(2, fromId); debit.executeUpdate(); credit.setBigDecimal(1, amount); credit.setString(2, toId); credit.executeUpdate(); conn.commit(); } // All three closed automatically // If exception thrown before commit, connection closed without commit // Most JDBC drivers rollback uncommitted transactions on close} // Java 9+ enhancement: effectively final variables can be usedpublic void java9Enhancement(Connection preexistingConnection) throws SQLException { // conn is effectively final (assigned once, never modified) Connection conn = preexistingConnection; // Java 9+ allows using it directly in try-with-resources try (conn) { // Use connection } // conn.close() called} // Combining with traditional catch/finallypublic void combinedForm() throws IOException { try (var reader = new BufferedReader(new FileReader("data.txt"))) { processContent(reader.readLine()); } catch (FileNotFoundException e) { // Handle specific exception handleMissingFile(); } catch (IOException e) { // Handle other I/O errors logAndRethrow(e); } finally { // Runs AFTER close() has been called // Useful for additional cleanup or logging logOperationComplete(); }}How try-with-resources works under the hood:
The compiler transforms try-with-resources into a traditional try/finally block with careful exception handling. Here's the conceptual transformation:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// What you write:try (FileInputStream fis = new FileInputStream("file.txt")) { processFile(fis);} // What the compiler generates (conceptually):{ FileInputStream fis = new FileInputStream("file.txt"); Throwable primaryException = null; try { processFile(fis); } catch (Throwable t) { primaryException = t; throw t; } finally { if (fis != null) { if (primaryException != null) { try { fis.close(); } catch (Throwable suppressed) { // Key insight: exceptions from close() are SUPPRESSED // not lost, and not replacing the primary exception primaryException.addSuppressed(suppressed); } } else { fis.close(); // If no exception, close normally } } }} // Accessing suppressed exceptions:try { try (var resource = acquireResource()) { throw new BusinessException("Primary Error"); }} catch (BusinessException e) { System.out.println("Primary: " + e.getMessage()); Throwable[] suppressed = e.getSuppressed(); for (Throwable t : suppressed) { System.out.println("Suppressed: " + t.getMessage()); }}Before Java 7, if both the try block and the cleanup code threw exceptions, one would be lost. try-with-resources solves this with suppressed exceptions: the primary exception is preserved, and any exceptions from close() are attached as 'suppressed' exceptions, accessible via getSuppressed(). This ensures no exception information is lost.
C# adopted deterministic cleanup early with the IDisposable interface and using statement. Any type implementing IDisposable can be used in a using block, ensuring Dispose() is called when the block exits.
The IDisposable interface:
public interface IDisposable
{
void Dispose();
}
C# 8.0 introduced using declarations that reduce nesting and improve readability by scoping disposal to the enclosing block.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
using System;using System.IO;using System.Data.SqlClient; // Traditional using statementpublic void ReadFile(string path){ using (StreamReader reader = new StreamReader(path)) { string line; while ((line = reader.ReadLine()) != null) { ProcessLine(line); } } // reader.Dispose() called here} // Multiple resources - various nesting stylespublic void CopyFile(string source, string dest){ // Stacked using statements (older style) using (FileStream sourceStream = File.OpenRead(source)) using (FileStream destStream = File.Create(dest)) { sourceStream.CopyTo(destStream); } // Both disposed in reverse order} // C# 8.0+ using declarations (scope-based)public void ModernStyle(string path){ using var reader = new StreamReader(path); // No braces! // reader is disposed at end of enclosing scope string content = reader.ReadToEnd(); ProcessContent(content); // Other code... } // reader disposed here, at end of method // Combining using declarations with other statementspublic async Task ProcessDataAsync(string connectionString){ await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM Users"; await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { yield return new User { Id = reader.GetInt32(0), Name = reader.GetString(1) }; }} // reader, command, connection all disposed asynchronously // IAsyncDisposable for async cleanup (C# 8.0+)public class AsyncDatabaseConnection : IAsyncDisposable{ private SqlConnection _connection; public async ValueTask DisposeAsync() { if (_connection != null) { await _connection.CloseAsync(); await _connection.DisposeAsync(); } }} // Usage with await usingpublic async Task UseAsyncResource(){ await using var db = new AsyncDatabaseConnection(); await db.QueryAsync("SELECT 1");}The Dispose Pattern:
C# has a canonical pattern for implementing IDisposable, particularly when your type holds both managed and unmanaged resources. This pattern also integrates with finalizers as a safety net.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
// The canonical Dispose patternpublic class ResourceHolder : IDisposable{ // Managed resources (other IDisposable objects) private FileStream _fileStream; private SqlConnection _connection; // Unmanaged resources (handles, pointers) private IntPtr _unmanagedHandle; // Track disposal state private bool _disposed = false; // Constructor public ResourceHolder(string filePath, string connectionString) { _fileStream = new FileStream(filePath, FileMode.Open); _connection = new SqlConnection(connectionString); _unmanagedHandle = NativeMethods.AllocateHandle(); } // Public Dispose method - called by users public void Dispose() { Dispose(disposing: true); // Tell GC not to call finalizer - we already cleaned up GC.SuppressFinalize(this); } // Protected virtual method - can be overridden by derived classes protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Dispose managed resources // These have their own Dispose methods _fileStream?.Dispose(); _connection?.Dispose(); } // Always dispose unmanaged resources // (whether called from Dispose or finalizer) if (_unmanagedHandle != IntPtr.Zero) { NativeMethods.FreeHandle(_unmanagedHandle); _unmanagedHandle = IntPtr.Zero; } _disposed = true; } // Finalizer - safety net for unmanaged resources only ~ResourceHolder() { // disposing: false means only clean up unmanaged resources // Managed resources might already be finalized! Dispose(disposing: false); } // Methods should check disposed state public void DoWork() { if (_disposed) throw new ObjectDisposedException(nameof(ResourceHolder)); // ... actual work ... }} // Simpler pattern for managed-only resources (C# 8.0+)public sealed class SimpleManagedResource : IDisposable{ private FileStream _stream; private bool _disposed; public void Dispose() { if (!_disposed) { _stream?.Dispose(); _disposed = true; } } // No finalizer needed - no unmanaged resources}The full Dispose pattern with a finalizer is only needed when your class directly holds unmanaged resources. If you only wrap other IDisposable objects, use the simpler sealed pattern. If you have no resources at all, you don't need IDisposable. Most application code uses existing disposable types and rarely needs to implement the full pattern.
Python's with statement and context managers provide elegant resource management. The context manager protocol consists of two methods:
__enter__() — Called when entering the with block; return value is bound to the as variable__exit__(exc_type, exc_val, exc_tb) — Called when exiting the block; receives exception info if one occurredThe contextlib module provides utilities for creating context managers easily.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
# Built-in file objects are context managerswith open('data.txt', 'r') as f: content = f.read() process(content)# f.close() called automatically # Multiple context managers in one with statementwith open('input.txt') as src, open('output.txt', 'w') as dst: for line in src: dst.write(line.upper())# Both files closed # Custom context manager classclass DatabaseConnection: """Context manager for database connections.""" def __init__(self, connection_string): self.connection_string = connection_string self.connection = None def __enter__(self): """Acquire resource and return it (or a proxy).""" self.connection = psycopg2.connect(self.connection_string) return self.connection # This is bound to 'as' variable def __exit__(self, exc_type, exc_val, exc_tb): """Release resource. Handle or propagate exceptions.""" if self.connection: if exc_type is not None: # An exception occurred - rollback self.connection.rollback() self.connection.close() # Return False (or None) to propagate exceptions # Return True to suppress the exception return False # Usagewith DatabaseConnection('postgresql://localhost/mydb') as conn: cursor = conn.cursor() cursor.execute('SELECT * FROM users') users = cursor.fetchall()# Connection automatically closed, with rollback on error # Transaction manager with commit on successclass Transaction: def __init__(self, connection): self.connection = connection self.cursor = None def __enter__(self): self.cursor = self.connection.cursor() return self.cursor def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: # No exception - commit self.connection.commit() else: # Exception occurred - rollback self.connection.rollback() self.cursor.close() return False # Don't suppress exceptions # Nested context managers for complex scenarioswith DatabaseConnection(conn_str) as conn: with Transaction(conn) as cursor: cursor.execute('INSERT INTO logs VALUES (%s)', (message,))# Transaction committed, connection closed # Exception suppression exampleclass SuppressingLockManager: """Silently handles lock acquisition failure.""" def __init__(self, lock): self.lock = lock self.acquired = False def __enter__(self): try: self.acquired = self.lock.acquire(timeout=1.0) except LockError: self.acquired = False return self def __exit__(self, exc_type, exc_val, exc_tb): if self.acquired: self.lock.release() # Suppress LockError but not other exceptions if exc_type is LockError: return True # Suppress return False # Propagate with SuppressingLockManager(lock) as mgr: if mgr.acquired: do_work() else: log("Lock unavailable, skipping")Using contextlib for simpler context managers:
Python's contextlib module provides decorators and utilities that make creating context managers much easier:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
from contextlib import contextmanager, closing, suppress, ExitStack # @contextmanager decorator - generator-based context managers@contextmanagerdef managed_connection(connection_string): """Create a context manager using a generator.""" connection = psycopg2.connect(connection_string) try: yield connection # What __enter__ returns except Exception: connection.rollback() raise else: connection.commit() finally: connection.close() # Always runs # Usagewith managed_connection('postgresql://localhost/mydb') as conn: conn.execute('INSERT INTO users VALUES (%s)', ('Alice',)) # @contextmanager for temporary state changes@contextmanager def temporary_directory_change(new_dir): """Temporarily change working directory.""" old_dir = os.getcwd() os.chdir(new_dir) try: yield finally: os.chdir(old_dir) with temporary_directory_change('/tmp'): # Working directory is /tmp here create_temp_files()# Back to original directory # closing() - adapt any object with close() methodfrom urllib.request import urlopen with closing(urlopen('https://example.com')) as page: content = page.read()# page.close() called # suppress() - cleanly ignore specific exceptionsfrom contextlib import suppress with suppress(FileNotFoundError): os.remove('might_not_exist.txt')# No exception raised if file doesn't exist # ExitStack - manage dynamic number of context managersfrom contextlib import ExitStack def process_all_files(file_paths): with ExitStack() as stack: # Dynamically open all files files = [stack.enter_context(open(fp)) for fp in file_paths] # All files are open here for f in files: process(f.read()) # ALL files automatically closed # Async context managers (Python 3.7+)from contextlib import asynccontextmanager @asynccontextmanagerasync def managed_async_connection(url): connection = await aiohttp.connect(url) try: yield connection finally: await connection.close() async def fetch_data(): async with managed_async_connection('wss://api.example.com') as ws: await ws.send('hello') response = await ws.receive()# Connection closed after blockThe @contextmanager decorator is incredibly powerful. The code before 'yield' is enter, the yielded value is bound to 'as', and the code after 'yield' is exit. Use try/finally to ensure cleanup. This pattern turns any acquire/use/release pattern into a context manager with minimal boilerplate.
Different languages have adopted various approaches to deterministic resource cleanup. Understanding these variations helps you apply the same principles regardless of which language you're using.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Kotlin's use() extension function - cleaner than Java's try-with-resources // Basic usage - any Closeable/AutoCloseablefun readFile(path: String): String { val file = File(path) return file.bufferedReader().use { reader -> reader.readText() } // reader.close() called} // use() returns the lambda's resultfun processData(): List<User> { return File("users.json").inputStream().use { stream -> JsonParser.parse(stream) }} // Multiple resources - nested use()fun copyFile(source: Path, dest: Path) { Files.newInputStream(source).use { input -> Files.newOutputStream(dest).use { output -> input.copyTo(output) } } // Both closed in reverse order} // Custom use-like extension for any resourceinline fun <T : AutoCloseable?, R> T.useWith(block: (T) -> R): R { var exception: Throwable? = null try { return block(this) } catch (e: Throwable) { exception = e throw e } finally { when { this == null -> {} exception == null -> close() else -> try { close() } catch (closeException: Throwable) { exception.addSuppressed(closeException) } } }} // Kotlin scope functions complement use()fun executeQuery(sql: String): List<Row> { return dataSource.connection.use { conn -> conn.prepareStatement(sql).use { stmt -> stmt.executeQuery().use { rs -> generateSequence { if (rs.next()) rs.toRow() else null } .toList() } } }} // Coroutine-aware resource managementsuspend fun asyncOperation() { val resource = acquireResource() try { useResourceAsync(resource) } finally { withContext(NonCancellable) { resource.close() // Cleanup even if coroutine cancelled } }}| Language | Mechanism | Scope | Exception Handling |
|---|---|---|---|
| Java | try-with-resources | Block | Suppressed exceptions attached to primary |
| C# | using statement/declaration | Block or method | Exception propagated, Dispose still called |
| Python | with statement | Block | exit receives exception info, can suppress |
| Kotlin | use() extension | Lambda body | Suppressed exceptions (Java-compatible) |
| Go | defer statement | Function | Deferred funcs run on all exit paths including panic |
| Swift | defer statement | Scope (loop iter/block) | Defers run regardless of how scope exits |
| Rust | Drop trait | Variable lifetime | Panic unwind calls destructors |
When creating types that manage resources, implementing the appropriate interface (AutoCloseable, IDisposable, context manager protocol) allows users to leverage language cleanup mechanisms. Here are comprehensive examples of well-designed resource types.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Well-designed AutoCloseable implementationpublic class ManagedDatabaseSession implements AutoCloseable { private final Connection connection; private final List<PreparedStatement> statements = new ArrayList<>(); private final List<ResultSet> resultSets = new ArrayList<>(); private boolean closed = false; public ManagedDatabaseSession(DataSource dataSource) throws SQLException { // Acquire resource in constructor this.connection = dataSource.getConnection(); this.connection.setAutoCommit(false); } public PreparedStatement prepare(String sql) throws SQLException { checkOpen(); PreparedStatement stmt = connection.prepareStatement(sql); statements.add(stmt); // Track for cleanup return stmt; } public ResultSet executeQuery(PreparedStatement stmt) throws SQLException { checkOpen(); ResultSet rs = stmt.executeQuery(); resultSets.add(rs); // Track for cleanup return rs; } public void commit() throws SQLException { checkOpen(); connection.commit(); } public void rollback() throws SQLException { checkOpen(); connection.rollback(); } @Override public void close() throws SQLException { if (closed) return; // Idempotent closed = true; SQLException exceptions = null; // Close in reverse order of acquisition // ResultSets first for (int i = resultSets.size() - 1; i >= 0; i--) { try { resultSets.get(i).close(); } catch (SQLException e) { if (exceptions == null) exceptions = e; else exceptions.addSuppressed(e); } } // Then statements for (int i = statements.size() - 1; i >= 0; i--) { try { statements.get(i).close(); } catch (SQLException e) { if (exceptions == null) exceptions = e; else exceptions.addSuppressed(e); } } // Finally connection try { connection.close(); } catch (SQLException e) { if (exceptions == null) exceptions = e; else exceptions.addSuppressed(e); } if (exceptions != null) throw exceptions; } private void checkOpen() { if (closed) { throw new IllegalStateException("Session is closed"); } }} // Usagetry (var session = new ManagedDatabaseSession(dataSource)) { PreparedStatement stmt = session.prepare("SELECT * FROM users WHERE id = ?"); stmt.setLong(1, userId); ResultSet rs = session.executeQuery(stmt); while (rs.next()) { process(rs); } session.commit();} // Everything closed: rs, stmt, connectionWhen implementing closeable types: (1) Make close/dispose idempotent—safe to call multiple times; (2) Track all sub-resources and close them in reverse order; (3) Aggregate exceptions rather than losing them; (4) Throw on methods called after close; (5) Document whether close is thread-safe.
Real-world resource management often involves patterns beyond basic try-with-resources. Here are advanced patterns that address common scenarios.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
// Pattern: Loan Pattern - provide resource for callback durationpublic <T> T withConnection(Function<Connection, T> operation) throws SQLException { try (Connection conn = dataSource.getConnection()) { return operation.apply(conn); }} // Usage - caller never sees the resource lifecycleString result = withConnection(conn -> { PreparedStatement stmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?"); stmt.setLong(1, userId); ResultSet rs = stmt.executeQuery(); return rs.next() ? rs.getString(1) : null;}); // Pattern: Resource Factory returning AutoCloseable wrapperpublic AutoCloseable createTrackedResource() { NativeResource resource = NativeResource.allocate(); return () -> { resource.free(); metrics.recordRelease(); };} // Pattern: Composite resource managing multiple sub-resourcespublic class ResourceBundle implements AutoCloseable { private final List<AutoCloseable> resources = new ArrayList<>(); public <T extends AutoCloseable> T add(T resource) { resources.add(resource); return resource; } @Override public void close() throws Exception { Exception first = null; for (int i = resources.size() - 1; i >= 0; i--) { try { resources.get(i).close(); } catch (Exception e) { if (first == null) first = e; else first.addSuppressed(e); } } if (first != null) throw first; }} // Usagetry (ResourceBundle bundle = new ResourceBundle()) { Connection conn = bundle.add(dataSource.getConnection()); FileInputStream fis = bundle.add(new FileInputStream("data.txt")); // Use both resources} // Both closed properly // Pattern: Conditional resource acquisitionpublic void processWithOptionalCache(String key) throws Exception { Cache cache = cacheEnabled ? new Cache() : null; try { if (cache != null) { // Use cache } // Process } finally { if (cache != null) { cache.close(); } }} // Better with try-with-resources for optional:try (Cache cache = cacheEnabled ? new Cache() : null) { // Works! null resources are safely handled} // Pattern: Transfer ownershippublic FileInputStream transferOwnership() { FileInputStream fis = new FileInputStream("data.txt"); // Successfully created - transfer ownership to caller // Caller is now responsible for closing return fis;} // Pattern: Wrapper that takes ownershippublic class OwnedInputStream implements AutoCloseable { private final InputStream delegate; private final boolean ownsDelegate; public OwnedInputStream(InputStream delegate, boolean ownsDelegate) { this.delegate = delegate; this.ownsDelegate = ownsDelegate; } @Override public void close() throws IOException { if (ownsDelegate) { delegate.close(); } }}The Loan Pattern (also called Execute Around) is particularly powerful. Instead of returning a resource that the caller must manage, you loan the resource for the duration of a callback. The caller focuses on what to do with the resource; lifecycle management is completely hidden. This is the gold standard for API design involving resources.
Deterministic resource cleanup is essential for building reliable software. Whether you're using Java's try-with-resources, C#'s using, Python's with, or Go's defer, the principles remain the same: tie resource lifetime to lexical scope, ensure cleanup happens regardless of control flow, and handle errors gracefully.
What's Next:
Building on the language-level constructs we've explored, the next page examines scope-based resource management in more depth—how to design systems where resource lifetimes are precisely controlled by code structure, making correct cleanup not just possible but automatic and inevitable.
You now have comprehensive knowledge of deterministic cleanup patterns across major programming languages. These patterns eliminate resource leaks, simplify error handling, and make your code more maintainable. Apply these patterns consistently, and resource management becomes automatic rather than error-prone.