TypeScript `using` in Real Codebases: Database Connections, File Handles, and Async Disposal Done Right A developer outlines how the `using` and `await using` declarations in ES2026 and TypeScript 5.2 guarantee deterministic resource disposal at scope exit, eliminating leaked database connections and file handles. The writeup explains that `Symbol.dispose` and `Symbol.asyncDispose` are called automatically on normal return, throw, or break, with resources disposed in reverse declaration order and errors aggregated via `SuppressedError`. using in Real Codebases: Database Connections, File Handles, and Async Disposal Done Right This article was written with the assistance of AI, under human supervision and review. Most resource leak bugs stem from a single assumption: developers remember to close what they open. Production codebases overflow with database connections held open by early returns, file handles left dangling after exceptions, and transaction scopes rolled back inconsistently because the cleanup logic sits five functions away from the acquisition site. The using keyword shipped in ES2026 and TypeScript 5.2 eliminates this entire class of failure by guaranteeing disposal at scope exit. When a resource marked with using goes out of scope, the runtime calls its Symbol.dispose method automatically. No try/finally scaffolding. No manual cleanup tracking. No leaked connections because an engineer forgot to close a stream in the error path. Traditional resource management requires explicit cleanup in every exit path. Miss one and the connection leaks. Add a new return statement and the leak reappears. The cognitive load compounds in async contexts where finally blocks must await disposal calls and race conditions turn subtle. The using declaration makes disposal deterministic. The connection closes when the block ends, whether by normal return, throw, or break. The disposal happens in the correct order when multiple resources stack. Async disposal via await using handles asynchronous cleanup without race conditions. The pattern scales from file handles to distributed locks without new footguns. await using handles async disposal correctly by awaiting the Symbol.asyncDispose method before continuing execution, preventing race conditions in asynchronous resource cleanup. The using keyword marks a variable declaration as disposable. When the containing block exits, the runtime calls the Symbol.dispose method on the value. The disposal happens synchronously. If the method throws, the error propagates after the block completes. class FileHandle { constructor private fd: number {} Symbol.dispose { // Called automatically at scope exit closeSync this.fd ; } } function processFile path: string { using handle = new FileHandle openSync path ; // Use the file handle // Disposal happens here when scope ends } The await using variant handles asynchronous disposal. The Symbol.asyncDispose method returns a promise. The runtime awaits it before continuing. This matters for network connections, database transactions, and distributed locks where cleanup requires async operations. class DatabaseConnection { constructor private conn: Connection {} async Symbol.asyncDispose { // Called automatically at scope exit await this.conn.close ; } } async function runQuery sql: string { await using conn = await pool.acquire ; return await conn.query sql ; // Disposal awaited here before function returns } The disposal order matters. When multiple using declarations exist in the same scope, they dispose in reverse order of declaration. This matches the stack-unwinding semantics developers expect from nested resource acquisition. The disposal happens even when exceptions occur. If the block throws, the runtime calls Symbol.dispose on all declared resources before propagating the error. If disposal itself throws, the runtime aggregates the errors using SuppressedError . The original error remains the primary exception and the disposal error attaches as the suppressed property. Database connection leaks destroy production systems. A connection held open for ten seconds too long under load means hundreds of requests queued behind it. Teams add connection pool monitoring, implement timeouts, and still find connections leaking in error paths because manual cleanup logic lives three layers away from the acquisition site. The disposable connection pattern wraps pool connections with automatic release. The wrapper acquires a connection on construction and releases it in Symbol.asyncDispose . When the connection goes out of scope, it returns to the pool immediately. No timeouts. No manual release calls. No leaked connections because an engineer forgot to call release in the error handler. class DisposableConnection { private constructor private conn: PoolConnection, private pool: Pool {} static async acquire pool: Pool : Promise