The using Keyword and Disposable Pattern in TypeScript
Learn TypeScript's using keyword for automatic resource cleanup with the Disposable and AsyncDisposable interfaces.
What you'll learn
- ✓How the using keyword automatically disposes resources
- ✓Implement Disposable and AsyncDisposable interfaces
- ✓Replace try/finally cleanup patterns with using
Prerequisites
- •Basic TypeScript classes and interfaces
- •Understanding of async/await
TypeScript 5.2 introduced the using keyword, implementing the TC39 Explicit Resource Management proposal. It provides a deterministic way to clean up resources like file handles, database connections, and locks when they go out of scope. Think of it as a try/finally block that the language handles for you.
The Problem: Manual Cleanup
Without using, resource cleanup requires careful try/finally blocks.
async function processFile(path: string): Promise<void> {
const file = await openFile(path);
try {
const data = await file.read();
await processData(data);
} finally {
await file.close(); // Easy to forget!
}
}
Every resource needs its own finally block. Nested resources get deeply indented. Forgetting a finally causes resource leaks. The using keyword solves all of this.
The Disposable Interface
A resource is disposable if it implements Symbol.dispose (synchronous) or Symbol.asyncDispose (asynchronous).
class TempFile implements Disposable {
private path: string;
private handle: number;
constructor(prefix: string) {
this.path = `/tmp/${prefix}-${Date.now()}.tmp`;
this.handle = openSync(this.path);
console.log(`Created: ${this.path}`);
}
write(data: string): void {
writeSync(this.handle, data);
}
read(): string {
return readSync(this.handle);
}
[Symbol.dispose](): void {
closeSync(this.handle);
unlinkSync(this.path);
console.log(`Disposed: ${this.path}`);
}
}
Using the using Keyword
Declare a resource with using and it is automatically disposed when it leaves scope.
function processData(): void {
using tempFile = new TempFile('data');
tempFile.write('hello world');
const content = tempFile.read();
console.log(content);
// tempFile[Symbol.dispose]() is called automatically here
}
No try/finally needed. The disposal happens at the end of the block, even if an exception is thrown.
Async Disposal With await using
For async cleanup, implement AsyncDisposable and use await using.
class DatabaseConnection implements AsyncDisposable {
private client: any;
private constructor(client: any) {
this.client = client;
}
static async connect(url: string): Promise<DatabaseConnection> {
const client = await createClient(url);
await client.connect();
console.log('Connected to database');
return new DatabaseConnection(client);
}
async query(sql: string, params?: unknown[]): Promise<unknown[]> {
return this.client.query(sql, params);
}
async [Symbol.asyncDispose](): Promise<void> {
await this.client.disconnect();
console.log('Disconnected from database');
}
}
async function getUsers(): Promise<unknown[]> {
await using db = await DatabaseConnection.connect('postgres://localhost/mydb');
const users = await db.query('SELECT * FROM users');
return users;
// db[Symbol.asyncDispose]() is called automatically
}
Multiple Resources
When you declare multiple using variables in the same block, they are disposed in reverse order (last in, first out), just like nested try/finally blocks.
async function transfer(fromId: string, toId: string, amount: number): Promise<void> {
await using db = await DatabaseConnection.connect('postgres://localhost/mydb');
await using lock = await acquireLock(`transfer-${fromId}-${toId}`);
await using tx = await db.beginTransaction();
await tx.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
await tx.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
await tx.commit();
// Disposed in reverse order: tx -> lock -> db
}
Building a Disposable Lock
class Mutex {
private locked = false;
private waiters: (() => void)[] = [];
async acquire(): Promise<Disposable> {
while (this.locked) {
await new Promise<void>((resolve) => this.waiters.push(resolve));
}
this.locked = true;
return {
[Symbol.dispose]: () => {
this.locked = false;
const next = this.waiters.shift();
if (next) next();
},
};
}
}
const mutex = new Mutex();
async function criticalSection(): Promise<void> {
using lock = await mutex.acquire();
// Only one caller executes this block at a time
await doExpensiveWork();
// Lock is released when the block exits
}
DisposableStack
The DisposableStack class lets you aggregate multiple disposable resources and dispose them together.
function setupResources(): Disposable {
const stack = new DisposableStack();
const logger = stack.use(createLogger());
const cache = stack.use(createCache());
const metrics = stack.use(createMetricsCollector());
// If any resource creation fails, already-added resources are disposed
return stack; // DisposableStack implements Disposable
}
function main(): void {
using resources = setupResources();
// All resources are available
// All are disposed when the block exits
}
The stack.use() method registers a disposable resource. If a later registration throws, all previously registered resources are disposed in reverse order. There is also stack.defer() for arbitrary cleanup callbacks.
function withTempDir(): Disposable {
const stack = new DisposableStack();
const dir = mkdtempSync('/tmp/app-');
stack.defer(() => {
rmSync(dir, { recursive: true });
console.log(`Removed temp dir: ${dir}`);
});
return stack;
}
Practical Example: HTTP Server Test Harness
class TestServer implements AsyncDisposable {
private server: any;
readonly port: number;
readonly baseUrl: string;
private constructor(server: any, port: number) {
this.server = server;
this.port = port;
this.baseUrl = `http://localhost:${port}`;
}
static async start(app: any): Promise<TestServer> {
return new Promise((resolve) => {
const server = app.listen(0, () => {
const port = server.address().port;
resolve(new TestServer(server, port));
});
});
}
async [Symbol.asyncDispose](): Promise<void> {
return new Promise((resolve) => {
this.server.close(() => {
console.log(`Test server on port ${this.port} stopped`);
resolve();
});
});
}
}
// In tests
async function testEndpoint(): Promise<void> {
await using server = await TestServer.start(app);
const response = await fetch(`${server.baseUrl}/api/health`);
assert(response.ok);
// Server is stopped automatically after the test
}
Configuration Requirements
To use using, set the following compiler options.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "ESNext.Disposable"]
}
}
If targeting older runtimes, you may need a polyfill for Symbol.dispose and Symbol.asyncDispose.
When to Use using
Use using for any resource that requires deterministic cleanup: file handles, database connections, network sockets, locks and semaphores, temporary files and directories, timers and intervals, and event listener registrations.
Do not use it for resources with indefinite lifetimes, like a database connection pool that lives for the entire application. Those are typically managed at the application level, not per-scope.
Wrap Up
The using keyword brings automatic resource management to TypeScript. Implement Symbol.dispose or Symbol.asyncDispose on your classes, declare them with using or await using, and the runtime handles cleanup when the scope exits. This eliminates forgotten finally blocks, simplifies nested resource management, and makes resource leaks a compile-time concern rather than a runtime surprise.
Related articles
- TypeScript tsconfig.json Deep Dive: Every Option Explained
A comprehensive guide to tsconfig.json covering compiler options, module resolution, path aliases, project references, and recommended configurations.
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Type Guards and Narrowing Explained
Learn how TypeScript type guards and control flow narrowing work, including typeof, instanceof, in, discriminated unions, and custom type predicates.
- TypeScript TypeScript Utility Types: The Complete Guide
A comprehensive guide to all built-in TypeScript utility types including Partial, Required, Pick, Omit, Record, Extract, Exclude, and more with practical examples.