Explicit Resource Management: using and Symbol.dispose
JavaScript's answer to RAII — automatic cleanup for file handles, locks, and event listeners that actually runs.
JavaScript has always struggled with cleanup. We've used try/finally, manual cleanup in
useEffect returns, and hoped we didn't forget. Now ECMAScript's Explicit Resource Management
brings the using keyword and Symbol.dispose — automatic, guaranteed cleanup
when a resource goes out of scope. Think C++ RAII or C# using, but for JavaScript.
The using Declaration
// ✅ Resource is ALWAYS disposed when block exits
{
using file = await openFile('data.txt');
const content = file.read();
processContent(content);
// file is disposed here — even if processContent throws
}
// ✅ Works with functions
function processData() {
using lock = acquireLock();
// ... do work ...
return result;
// lock released after return value is captured, before function exits
}
The using keyword declares a variable whose value will be disposed when the enclosing
scope exits — whether normally, via return, or via exception. The value must have a
[Symbol.dispose]() method or be null/undefined.
Making Objects Disposable
class DatabaseConnection {
#connection;
#disposed = false;
constructor(connectionString) {
this.#connection = connect(connectionString);
}
query(sql) {
if (this.#disposed) {
throw new Error('Connection already disposed');
}
return this.#connection.query(sql);
}
// The disposal method
[Symbol.dispose]() {
if (!this.#disposed) {
this.#connection.close();
this.#disposed = true;
}
}
}
// Usage
{
using db = new DatabaseConnection('postgres://...');
const users = db.query('SELECT * FROM users');
// db.close() called automatically here
}
Implement [Symbol.dispose]() to define cleanup logic. It should be idempotent —
safe to call multiple times. Don't throw unless cleanup truly failed.
Async Disposal: await using
class AsyncResource {
async [Symbol.asyncDispose]() {
await this.flushBuffers();
await this.closeConnection();
}
}
// Use await using for async disposal
async function process() {
await using resource = new AsyncResource();
await resource.doWork();
// asyncDispose awaited here
}
Use Symbol.asyncDispose when cleanup is async. The await using
declaration awaits the disposal promise before the scope exits.
Practical Patterns
Event Listener Cleanup
function createEventDisposable(target, event, handler) {
target.addEventListener(event, handler);
return {
[Symbol.dispose]() {
target.removeEventListener(event, handler);
}
};
}
function trackMousePosition() {
using listener = createEventDisposable(
document,
'mousemove',
e => console.log(e.clientX, e.clientY)
);
// ... do something ...
// Listener removed automatically when function exits
}AbortController Cleanup
function createAbortDisposable() {
const controller = new AbortController();
return {
signal: controller.signal,
[Symbol.dispose]() {
controller.abort('Scope exited');
}
};
}
async function fetchWithAutoCancel(url) {
using abortable = createAbortDisposable();
try {
return await fetch(url, { signal: abortable.signal });
} catch (e) {
if (e.name === 'AbortError') return null;
throw e;
}
// Request cancelled if we exit early
}Mutex/Lock Pattern
class Lock {
#locked = false;
#queue = [];
async acquire() {
if (this.#locked) {
await new Promise(resolve => this.#queue.push(resolve));
}
this.#locked = true;
return {
[Symbol.dispose]: () => {
this.#locked = false;
this.#queue.shift()?.();
}
};
}
}
const lock = new Lock();
async function criticalSection() {
using _guard = await lock.acquire();
// Only one caller at a time reaches here
await doExclusiveWork();
// Lock released when block exits
}Timer Cleanup
function createTimeout(ms) {
let id;
const promise = new Promise(resolve => {
id = setTimeout(resolve, ms);
});
return {
promise,
[Symbol.dispose]() {
clearTimeout(id);
}
};
}
async function withTimeout() {
using timer = createTimeout(5000);
// If we return early, the timeout is cleared
if (cachedResult) return cachedResult;
await timer.promise;
return computeExpensiveResult();
}DisposableStack: Managing Multiple Resources
async function complexOperation() {
using stack = new DisposableStack();
// Add resources in order; disposed in reverse order
const db = stack.use(new DatabaseConnection());
const file = stack.use(await openFile('log.txt'));
const lock = stack.use(await mutex.acquire());
// All three disposed when function exits,
// in order: lock, file, db
await db.query('INSERT...');
await file.write('Success');
}
// For async disposal
async function asyncComplex() {
await using stack = new AsyncDisposableStack();
stack.use(await asyncResource1());
stack.defer(async () => await cleanup());
// All disposals awaited in reverse order
}
DisposableStack collects disposables and disposes them in reverse order.
Use defer() to add arbitrary cleanup callbacks.
Error Handling: SuppressedError
When disposal throws, JavaScript doesn't lose the original error:
class FailingResource {
[Symbol.dispose]() {
throw new Error('Disposal failed');
}
}
try {
using resource = new FailingResource();
throw new Error('Main error');
} catch (e) {
// e is a SuppressedError
console.log(e.error); // Error: Disposal failed
console.log(e.suppressed); // Error: Main error
}
SuppressedError chains all errors — the disposal error becomes error,
the original exception becomes suppressed. Multiple disposal errors nest further.
Edge Cases and Gotchas
Closure Escape
function danger() {
using resource = new Resource();
// ⚠️ Returning a closure that captures the resource
return () => resource.doWork();
// Resource disposed BEFORE function returns!
}
const fn = danger();
fn(); // Error: Resource already disposedThe resource is disposed when the function scope exits, but the closure still holds a reference. Avoid returning closures that capture disposed resources.
No Top-Level Script
// ❌ Not allowed in scripts (scope never exits)
using resource = new Resource(); // SyntaxError
// ✅ Allowed in modules (disposed when module finishes loading)
using resource = new Resource(); // OK in .mjsReverse Order Matters
using a = new ResourceA();
using b = new ResourceB(a); // b depends on a
// When scope exits: b disposed first, then a
// This is correct — dependent resources cleaned up firstBrowser & TypeScript Support
As of 2026: Chrome 127+, Safari 18+, Firefox 132+. TypeScript 5.2+ supports
using natively with the lib: ["esnext.disposable"] config.
For older environments, use the core-js polyfill:
import "core-js/proposals/explicit-resource-management";
// Feature detection
if (Symbol.dispose) {
// Native support
}Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement