
using and await using: explicit resource management in JavaScript
The problem of forgetting to close a resource on an early exception, how Symbol.dispose/Symbol.asyncDispose work, and a practical example compared to try/finally.
Any resource that needs explicit cleanup — a database connection, a file handle, a lock — shares the same failure mode: it’s easy to forget the cleanup call on one of the exit paths, especially when an exception can skip straight past it. using and await using are new declarations designed specifically to make that mistake structurally hard to write.
The problem: cleanup that gets skipped on an exception
function processFile(path) {
const file = openFile(path);
const data = file.read(); // throws — parseData never runs, but file.close() also never runs
const result = parseData(data);
file.close();
return result;
}
If file.read() throws, execution jumps straight out of the function — file.close(), sitting after the line that failed, never runs. The resource leaks. The traditional fix is try/finally:
function processFile(path) {
const file = openFile(path);
try {
const data = file.read();
return parseData(data);
} finally {
file.close();
}
}
This works, but it’s opt-in and easy to forget — nothing forces you to wrap the resource in a try/finally, and with multiple resources, nesting gets unwieldy fast.
using: automatic cleanup, no try/finally needed
function processFile(path) {
using file = openFile(path);
const data = file.read(); // if this throws, file is still disposed automatically
return parseData(data);
}
using declares a variable the same way const does, but it also registers the value for automatic cleanup at the end of the enclosing block — whether that block exits normally, via return, or via a thrown exception. No finally block to remember, no risk of a cleanup call sitting after a line that can throw.
How it works: Symbol.dispose
For a value to work with using, it needs a method at Symbol.dispose — this is what actually gets called at block exit:
function openFile(path) {
const handle = nativeOpen(path);
return {
read: () => nativeRead(handle),
[Symbol.dispose]() {
nativeClose(handle);
},
};
}
You’re not limited to using using on library-provided resources — any object you author with a [Symbol.dispose] method plugs into the same mechanism.
await using: the async equivalent
Some cleanup is itself asynchronous — closing a database connection often needs to await a network round-trip. await using handles exactly that, calling [Symbol.asyncDispose] and awaiting it:
async function withConnection(query) {
await using conn = await openDatabaseConnection();
return await conn.execute(query);
// conn[Symbol.asyncDispose]() is called and awaited automatically here,
// whether execute() succeeded, returned early, or threw
}
function openDatabaseConnection() {
return {
execute: (query) => runQuery(query),
async [Symbol.asyncDispose]() {
await closeConnectionGracefully();
},
};
}
Multiple resources, cleaned up in reverse order
function processFiles(pathA, pathB) {
using fileA = openFile(pathA);
using fileB = openFile(pathB);
return merge(fileA.read(), fileB.read());
// fileB is disposed first, then fileA — reverse declaration order,
// the same convention try/finally nesting would require you to write by hand
}
This mirrors exactly how you’d nest try/finally blocks by hand for multiple resources — using just does it without the nesting.
⚠ Not for values without a dispose method
using/await using only work with values that implement Symbol.dispose/
Symbol.asyncDispose. Plain const remains correct for ordinary values — reach for using
specifically when you’re holding something that needs explicit teardown.
using vs try/finally at a glance
try/finally |
using / await using |
|
|---|---|---|
| Requires explicit block nesting | Yes | No |
| Easy to forget | Yes — nothing enforces it | Harder — declaration itself carries cleanup |
| Multiple resources | Nested blocks, verbose | Flat declarations, automatic reverse-order cleanup |
| Works with async cleanup | Yes, manually with await in finally |
Yes, natively with await using |
FAQ
❓ Do I need a library to use this today?
No polyfill needed for the syntax on engines that support it — Node.js and modern browsers have
been rolling out support. Check your target runtime’s version before relying on it without a
transpiler (Babel/TypeScript can downlevel-compile using for older targets).
❓ Can existing classes work with using without modification?
Only if they already implement Symbol.dispose or Symbol.asyncDispose, or you wrap them in a
small adapter object that does. It’s not automatic for arbitrary existing classes — you (or the
library author) need to add the method explicitly.
❓ Is this similar to Python's with statement or C#'s using?
Conceptually, yes — it’s the same category of feature: a declaration that ties a resource’s lifetime to a lexical scope and guarantees cleanup on exit. JavaScript’s version follows the same underlying idea those languages popularized.
Conclusion
using and await using remove the most common way resource cleanup gets skipped: an exception jumping past the line that was supposed to close things. Implement Symbol.dispose/Symbol.asyncDispose on anything that needs explicit teardown, and the language handles calling it — in the right order, on every exit path — without a try/finally block to write or forget.
References

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.