The using keyword enables automatic resource disposal when leaving scope. Here's how to use it effectively.
Basic Usage#
1// Disposable interface
2interface Disposable {
3 [Symbol.dispose](): void;
4}
5
6// Create disposable resource
7function createResource(): Disposable {
8 console.log('Resource acquired');
9
10 return {
11 [Symbol.dispose]() {
12 console.log('Resource disposed');
13 },
14 };
15}
16
17// Using declaration
18function example() {
19 using resource = createResource();
20 // Resource acquired
21
22 console.log('Using resource...');
23
24 // Resource automatically disposed when scope exits
25}
26// Resource disposed
27
28example();File Handling#
1import { open } from 'fs/promises';
2
3// File handle with disposal
4async function readFile(path: string) {
5 await using file = await open(path, 'r');
6
7 const content = await file.readFile('utf-8');
8 console.log(content);
9
10 // File handle automatically closed
11}
12
13// Multiple files
14async function copyFile(src: string, dest: string) {
15 await using srcFile = await open(src, 'r');
16 await using destFile = await open(dest, 'w');
17
18 const content = await srcFile.readFile();
19 await destFile.writeFile(content);
20
21 // Both files closed in reverse order
22}Database Connections#
1class DatabaseConnection implements Disposable {
2 private connected = false;
3
4 async connect() {
5 console.log('Connecting to database...');
6 this.connected = true;
7 return this;
8 }
9
10 query(sql: string) {
11 if (!this.connected) throw new Error('Not connected');
12 console.log(`Executing: ${sql}`);
13 return [];
14 }
15
16 [Symbol.dispose]() {
17 if (this.connected) {
18 console.log('Closing database connection');
19 this.connected = false;
20 }
21 }
22}
23
24async function getUserData(userId: number) {
25 using db = await new DatabaseConnection().connect();
26
27 const user = db.query(`SELECT * FROM users WHERE id = ${userId}`);
28
29 // Connection closed automatically
30 return user;
31}Lock Management#
1class Lock implements Disposable {
2 private static locks = new Map<string, Lock>();
3 private released = false;
4
5 private constructor(private name: string) {}
6
7 static acquire(name: string): Lock {
8 if (this.locks.has(name)) {
9 throw new Error(`Lock ${name} already held`);
10 }
11
12 const lock = new Lock(name);
13 this.locks.set(name, lock);
14 console.log(`Lock ${name} acquired`);
15 return lock;
16 }
17
18 [Symbol.dispose]() {
19 if (!this.released) {
20 Lock.locks.delete(this.name);
21 this.released = true;
22 console.log(`Lock ${this.name} released`);
23 }
24 }
25}
26
27function criticalSection() {
28 using lock = Lock.acquire('resource');
29
30 // Protected code
31 console.log('Doing critical work...');
32
33 // Lock released automatically
34}Async Disposable#
1interface AsyncDisposable {
2 [Symbol.asyncDispose](): Promise<void>;
3}
4
5class AsyncResource implements AsyncDisposable {
6 constructor(private name: string) {
7 console.log(`${name} created`);
8 }
9
10 async [Symbol.asyncDispose]() {
11 console.log(`${this.name} disposing...`);
12 await new Promise((resolve) => setTimeout(resolve, 100));
13 console.log(`${this.name} disposed`);
14 }
15}
16
17async function useAsyncResource() {
18 await using resource = new AsyncResource('MyResource');
19
20 console.log('Using async resource...');
21
22 // Async disposal awaited
23}Transaction Pattern#
1class Transaction implements Disposable {
2 private committed = false;
3 private rolledBack = false;
4
5 constructor(private db: Database) {
6 db.execute('BEGIN TRANSACTION');
7 console.log('Transaction started');
8 }
9
10 execute(sql: string) {
11 if (this.committed || this.rolledBack) {
12 throw new Error('Transaction already ended');
13 }
14 return this.db.execute(sql);
15 }
16
17 commit() {
18 if (!this.committed && !this.rolledBack) {
19 this.db.execute('COMMIT');
20 this.committed = true;
21 console.log('Transaction committed');
22 }
23 }
24
25 [Symbol.dispose]() {
26 if (!this.committed && !this.rolledBack) {
27 this.db.execute('ROLLBACK');
28 this.rolledBack = true;
29 console.log('Transaction rolled back');
30 }
31 }
32}
33
34function transferMoney(from: string, to: string, amount: number) {
35 using tx = new Transaction(db);
36
37 tx.execute(`UPDATE accounts SET balance = balance - ${amount} WHERE id = '${from}'`);
38 tx.execute(`UPDATE accounts SET balance = balance + ${amount} WHERE id = '${to}'`);
39
40 // Only commit if no errors
41 tx.commit();
42
43 // If error thrown, transaction auto-rolled back
44}Timer Cleanup#
1class Timer implements Disposable {
2 private intervalId: NodeJS.Timeout | null = null;
3
4 start(callback: () => void, interval: number) {
5 this.intervalId = setInterval(callback, interval);
6 console.log('Timer started');
7 return this;
8 }
9
10 [Symbol.dispose]() {
11 if (this.intervalId) {
12 clearInterval(this.intervalId);
13 this.intervalId = null;
14 console.log('Timer stopped');
15 }
16 }
17}
18
19function monitorStatus() {
20 using timer = new Timer().start(() => {
21 console.log('Checking status...');
22 }, 1000);
23
24 // Do work for 5 seconds
25 // Timer automatically cleaned up
26}Event Listener Cleanup#
1class EventSubscription implements Disposable {
2 constructor(
3 private target: EventTarget,
4 private type: string,
5 private handler: EventListener
6 ) {
7 target.addEventListener(type, handler);
8 console.log(`Subscribed to ${type}`);
9 }
10
11 [Symbol.dispose]() {
12 this.target.removeEventListener(this.type, this.handler);
13 console.log(`Unsubscribed from ${this.type}`);
14 }
15}
16
17function setupClickHandler(element: HTMLElement) {
18 using subscription = new EventSubscription(element, 'click', (e) => {
19 console.log('Clicked!', e);
20 });
21
22 // Do something...
23
24 // Listener removed when scope exits
25}DisposableStack#
1// Manage multiple disposables
2class DisposableStack implements Disposable {
3 private disposables: Disposable[] = [];
4
5 use<T extends Disposable>(disposable: T): T {
6 this.disposables.push(disposable);
7 return disposable;
8 }
9
10 defer(cleanup: () => void) {
11 this.disposables.push({
12 [Symbol.dispose]: cleanup,
13 });
14 }
15
16 [Symbol.dispose]() {
17 // Dispose in reverse order
18 while (this.disposables.length > 0) {
19 const disposable = this.disposables.pop()!;
20 disposable[Symbol.dispose]();
21 }
22 }
23}
24
25function complexOperation() {
26 using stack = new DisposableStack();
27
28 const conn1 = stack.use(new DatabaseConnection());
29 const conn2 = stack.use(new DatabaseConnection());
30
31 stack.defer(() => {
32 console.log('Additional cleanup');
33 });
34
35 // All resources disposed in reverse order
36}HTTP Client#
1class HttpClient implements AsyncDisposable {
2 private abortController = new AbortController();
3
4 async fetch(url: string) {
5 const response = await fetch(url, {
6 signal: this.abortController.signal,
7 });
8 return response.json();
9 }
10
11 async [Symbol.asyncDispose]() {
12 this.abortController.abort();
13 console.log('HTTP client disposed, pending requests cancelled');
14 }
15}
16
17async function fetchData() {
18 await using client = new HttpClient();
19
20 const data = await client.fetch('/api/data');
21 console.log(data);
22
23 // Any pending requests cancelled on disposal
24}Temporary Directory#
1import { mkdtemp, rm } from 'fs/promises';
2import { tmpdir } from 'os';
3import { join } from 'path';
4
5class TempDirectory implements AsyncDisposable {
6 private constructor(public readonly path: string) {}
7
8 static async create(): Promise<TempDirectory> {
9 const path = await mkdtemp(join(tmpdir(), 'app-'));
10 console.log(`Created temp directory: ${path}`);
11 return new TempDirectory(path);
12 }
13
14 async [Symbol.asyncDispose]() {
15 await rm(this.path, { recursive: true, force: true });
16 console.log(`Removed temp directory: ${this.path}`);
17 }
18}
19
20async function processFiles() {
21 await using tempDir = await TempDirectory.create();
22
23 // Use tempDir.path for temporary files
24 console.log(`Working in: ${tempDir.path}`);
25
26 // Directory automatically cleaned up
27}Best Practices#
Usage:
✓ Use for resource cleanup
✓ Implement Symbol.dispose
✓ Use await using for async
✓ Keep dispose idempotent
Patterns:
✓ File handles
✓ Database connections
✓ Locks and transactions
✓ Event subscriptions
Benefits:
✓ Automatic cleanup
✓ Exception safety
✓ Cleaner code
✓ No try/finally needed
Avoid:
✗ Long-lived resources
✗ Complex dispose logic
✗ Side effects in dispose
✗ Forgetting async disposal
Conclusion#
The using keyword provides automatic resource disposal in TypeScript. Implement Symbol.dispose for synchronous cleanup or Symbol.asyncDispose for async cleanup. Resources are disposed in reverse order of acquisition, and disposal happens even when exceptions occur. Use DisposableStack for managing multiple related resources.