TypeScript 87 🔷 The using Keyword and Disposable
Resource management in JavaScript has always been manual. You open a file handle, you close it. You acquire a database connection, you release it. You lock a mutex, you unlock it. The pattern is always the same: acquire, use, release. And the failure mode is always the same: if an exception is thrown between acquire and release, the release never happens. The resource leaks.
TypeScript 5.2 introduced the using keyword, based on the TC39 Explicit Resource Management proposal, which reached Stage 3 in 2023 . It is a language feature that automates the release step. Instead of writing try/finally around every resource, you declare it with using and the runtime calls its disposal method when the variable leaves scope. If an exception occurs, the disposal still happens. If you return early, the disposal still happens. The resource lifetime is tied to the lexical scope, and the release is guaranteed .
Key point: The using keyword works with any object that has a [Symbol.dispose] method. The await using variant works with any object that has a [Symbol.asyncDispose] method. When the scope exits — whether by normal completion, return, throw, or break — the disposal method is called. If multiple resources are declared in the same scope, they are disposed in reverse order of declaration, like a stack .
Why the using keyword exists
JavaScript has never had a standard mechanism for deterministic resource cleanup. Other languages do. C# has using. C++ has RAII. Python has context managers. Java has try-with-resources. JavaScript developers have relied on try/finally blocks, which are verbose, error-prone, and easy to forget.
The boilerplate problem. Every file operation in Node.js follows the same pattern: open the handle, wrap the work in try, close the handle in finally. The finally block runs whether the work succeeds or fails . This is correct, but it is five lines of scaffolding for every resource. A function that opens three resources has three try/finally blocks, or one deeply nested block. The using declaration reduces that scaffolding to a single keyword per resource .
The leak problem. The most common resource leak in JavaScript is the early return. A function opens a file handle, reads the contents, and returns early if the contents are empty. The close() call at the end of the function never runs. The handle leaks . With using, the disposal runs on scope exit regardless of how the scope exits. An early return, a thrown exception, a break out of a loop — all of them trigger the disposal.
The async problem. Many resources require asynchronous cleanup. A database connection needs to send a close message and wait for confirmation. A network socket needs to flush pending writes. The await using variant handles this: it awaits the [Symbol.asyncDispose] method before continuing .
The error-suppression problem. When an exception is thrown inside the try block, and the disposal in finally also throws, the second error replaces the first. The original cause is lost. The Explicit Resource Management proposal introduces SuppressedError, which contains both the error that was most recently thrown and the error that was suppressed . The using keyword uses this to preserve both exceptions.
The trade-off. The using keyword is a syntactic feature that requires runtime support. If the JavaScript engine does not implement Symbol.dispose and Symbol.asyncDispose, the code will not work without polyfills. The feature shipped in V8 v13.8 and Chromium 134 . Node.js and other runtimes adopted it later. For projects targeting older environments, polyfills are required .
a. The using Declaration and Symbol.dispose
The using declaration declares a block-scoped variable whose value must be an object with a [Symbol.dispose] method . When the scope exits, that method is called.
class Resource {
value = Math.random();
#isDisposed = false;
getValue() {
if (this.#isDisposed) {
throw new Error("Resource is disposed");
}
return this.value;
}
[Symbol.dispose]() {
this.#isDisposed = true;
console.log("Resource disposed");
}
}
{
using resource = new Resource();
console.log(resource.getValue());
} // "Resource disposed" is printed here
The using keyword works in any block scope: a function body, an if block, a for loop, or a plain { ... } block . It cannot be used at the top level of a script, because script scopes are persistent .
If the value assigned to a using variable is null or undefined, no disposal is attempted. This allows optional resources to be conditionally present .
If the value does not have a [Symbol.dispose] method, a TypeError is thrown when the declaration is evaluated .
b. await using and Symbol.asyncDispose
For resources that require asynchronous cleanup, await using uses [Symbol.asyncDispose] instead of [Symbol.dispose] . The disposal method is awaited before the scope continues.
const getConnection = async () => {
const connection = await getDb();
return {
connection,
[Symbol.asyncDispose]: async () => {
await connection.close();
},
};
};
{
await using db = await getConnection();
// Do stuff with db.connection
} // "db.connection.close()" is awaited here
The await using declaration can also operate on objects that only have [Symbol.dispose]. In that case, the synchronous disposer is called and its result is treated as already resolved .
The distinction matters for resources where the cleanup must complete before the program continues. A database connection that is not fully closed before the next query runs will produce confusing errors. The await using declaration ensures the close completes before the scope exits .
c. DisposableStack, AsyncDisposableStack, and Error Suppression
For cases where resources are created conditionally, or where cleanup actions do not correspond to a single object, the proposal introduces DisposableStack and AsyncDisposableStack .
A DisposableStack is a container that tracks disposable resources and runs their disposers when the stack itself is disposed. It has three methods for adding resources: use() adds a disposable object, adopt() adds a non-disposable object and a callback, and defer() adds a callback with no associated object .
{
using stack = new DisposableStack();
stack.defer(() => console.log("Cleanup 1"));
stack.defer(() => console.log("Cleanup 2"));
const resource = new Resource();
stack.use(resource);
} // Disposal order: resource, Cleanup 2, Cleanup 1
The DisposableStack disposes in first-in-last-out order, like a stack. The last resource added is the first disposed . This ensures that dependent resources are cleaned up before the resources they depend on.
SuppressedError is the new error type that handles the case where disposal throws while an exception is already propagating. It has an error property for the most recently thrown error and a suppressed property for the error that was suppressed by the new one . Without this, a disposal error would hide the original exception, making debugging harder.
Complete Example Session
This session builds a file-processing utility that uses using, await using, and DisposableStack to manage multiple resources with guaranteed cleanup.
// ============================================
// PART 1: THE BOILERPLATE PROBLEM
// ============================================
// Without using — manual cleanup
async function readFileManual(path: string) {
const handle = await open(path, "r");
try {
const content = await handle.readFile("utf-8");
return content;
} finally {
await handle.close();
}
}
// ============================================
// PART 2: THE USING DECLARATION
// ============================================
// Node.js file handles implement Symbol.dispose
async function readFileUsing(path: string) {
await using handle = await open(path, "r");
return await handle.readFile("utf-8");
} // handle.close() is called automatically
// ============================================
// PART 3: THE SYMBOL.DISPOSE INTERFACE
// ============================================
interface Disposable {
[Symbol.dispose](): void;
}
class Timer implements Disposable {
start = Date.now();
[Symbol.dispose]() {
console.log(`Timer ran for ${Date.now() - this.start}ms`);
}
}
{
using timer = new Timer();
// some work
} // prints elapsed time
// ============================================
// PART 4: THE ASYNC VARIANT
// ============================================
interface AsyncDisposable {
[Symbol.asyncDispose](): Promise<void>;
}
class DatabaseConnection implements AsyncDisposable {
async [Symbol.asyncDispose]() {
await this.close();
}
private async close() {
// send close message to database
}
}
{
await using db = new DatabaseConnection();
// run queries
} // db.close() is awaited before continuing
// ============================================
// PART 5: THE NULL AND UNDEFINED CASE
// ============================================
function getOptionalResource(shouldCreate: boolean): Disposable | null {
return shouldCreate ? new Timer() : null;
}
{
using resource = getOptionalResource(false);
// no disposal attempted, value is null
}
// ============================================
// PART 6: THE DISPOSABLE STACK
// ============================================
{
using stack = new DisposableStack();
const connection = await getConnection();
stack.use(connection);
stack.defer(() => {
console.log("Custom cleanup");
});
// ...
} // connection closed, then custom cleanup runs
// ============================================
// PART 7: THE SUPPRESSED ERROR
// ============================================
class FailingResource implements Disposable {
[Symbol.dispose]() {
throw new Error("Disposal failed");
}
}
try {
using resource = new FailingResource();
throw new Error("Original error");
} catch (e) {
// e is SuppressedError
// e.error is "Original error"
// e.suppressed is "Disposal failed"
}
// ============================================
// PART 8: THE CLOSURE TRAP
// ============================================
function createClosure() {
using resource = new Timer();
return () => resource.getValue(); // resource is disposed before this runs
}
// The resource is disposed when createClosure returns,
// even though the closure still holds a reference to it.
// ============================================
// PART 9: THE RUNTIME POLYFILL
// ============================================
// For environments without native support
if (typeof Symbol.dispose === "undefined") {
// @ts-ignore
Symbol.dispose = Symbol("Symbol.dispose");
}
if (typeof Symbol.asyncDispose === "undefined") {
// @ts-ignore
Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
}
// ============================================
// PART 10: THE PRACTICAL FILE UTILITY
// ============================================
async function processConfig(path: string) {
await using handle = await open(path, "r");
const content = await handle.readFile("utf-8");
if (!content) {
return null; // handle is still closed
}
return JSON.parse(content);
}
The ten parts cover the boilerplate problem, the using declaration, the Symbol.dispose interface, the async variant, the null case, the DisposableStack, the SuppressedError, the closure trap, the runtime polyfill, and the practical file utility.
Quick Reference
The Two Declarations
| Declaration | Symbol | Cleanup Type |
|---|---|---|
using x = ... | Symbol.dispose | Synchronous |
await using x = ... | Symbol.asyncDispose | Asynchronous |
The Interfaces
| Interface | Method | Purpose |
|---|---|---|
Disposable | [Symbol.dispose]() | Synchronous cleanup |
AsyncDisposable | [Symbol.asyncDispose]() | Asynchronous cleanup |
The DisposableStack Methods
| Method | Purpose |
|---|---|
use(value) | Add a disposable resource |
adopt(value, onDispose) | Add a resource with custom disposer |
defer(onDispose) | Add a cleanup callback only |
The Requirements
| Requirement | Detail |
|---|---|
| TypeScript version | 5.2+ |
| Compile target | es2022 or below |
| Lib setting | Include "esnext" or "esnext.disposable" |
| Runtime support | Polyfill Symbol.dispose for older runtimes |
The Scope Rules
| Context | Allowed |
|---|---|
Block { ... } | ✅ Yes |
| Function body | ✅ Yes |
for loop | ✅ Yes |
| Top-level script | ❌ No |
switch top-level | ❌ No |
Best Practices
✅ Do This:
// Use await using for async resources
await using handle = await open("file.txt", "r"); // ✅
// Use DisposableStack for conditional resources
using stack = new DisposableStack();
stack.defer(() => cleanup()); // ✅
// Set the correct lib setting
// tsconfig.json: "lib": ["esnext", "dom"] // ✅
// Polyfill symbols for older runtimes
if (!Symbol.dispose) Symbol.dispose = Symbol("dispose"); // ✅
❌ Don’t Do This:
// Don't use using at top level
using x = getResource(); // ❌ syntax error
// Don't forget await on async resources
using db = await getConnection(); // ❌ should be await using
// Don't rely on closure-held resources
return () => resource.getValue(); // ❌ resource already disposed
// Don't skip the polyfill for older runtimes
// The code will throw at the first using declaration // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Cannot find name ‘using'” | Missing lib setting | Add "esnext.disposable" to lib |
| Symbol.dispose is undefined | No runtime polyfill | Polyfill the symbols |
| Resource already disposed | Closure captured the resource | Move disposal inside the closure |
| TypeError on declaration | Object lacks [Symbol.dispose] | Implement the interface |
| Disposal error hides original | No SuppressedError handling | Use using — it preserves both |
Real-World Examples
1. File Handle
await using handle = await open("file.txt", "r");
2. Database Connection
await using db = new DatabaseConnection();
3. Custom Timer
class Timer { [Symbol.dispose]() { console.log("done"); } }
using t = new Timer();
4. DisposableStack with Defer
using stack = new DisposableStack();
stack.defer(() => console.log("cleanup"));
5. DisposableStack with Adopt
stack.adopt(nonDisposable, (v) => v.cleanup());
6. Async Disposal
class Conn { async [Symbol.asyncDispose]() { await this.close(); } }
await using c = new Conn();
7. Optional Resource
using resource = maybeResource ?? null;
8. SuppressedError
try { using r = new Failing(); throw new Error("original"); }
catch (e) { /* e.error, e.suppressed */ }
9. Polyfill
Symbol.dispose ??= Symbol("Symbol.dispose");
10. Lib Setting
{ "compilerOptions": { "lib": ["esnext", "dom"] } }
Visual
The using Lifecycle
┌──────────────────────────────────────────────┐
│ USING LIFECYCLE │
│ │
│ { │
│ using resource = new Resource(); │
│ // use resource │
│ } │
│ │ │
│ ▼ │
│ Scope exits (normal, return, throw, break) │
│ │ │
│ ▼ │
│ resource[Symbol.dispose]() is called │
│ │
│ Guaranteed to run. Always. │
│ │
└──────────────────────────────────────────────┘
The using vs try/finally
┌──────────────────────────────────────────────┐
│ TRY/FINALLY vs USING │
│ │
│ try/finally: │
│ const handle = await open(path); │
│ try { │
│ // work │
│ } finally { │
│ await handle.close(); │
│ } │
│ │
│ using: │
│ await using handle = await open(path); │
│ // work │
│ │
│ Same guarantee. Less syntax. │
│ │
└──────────────────────────────────────────────┘
The DisposableStack Order
┌──────────────────────────────────────────────┐
│ DISPOSABLE STACK ORDER │
│ │
│ stack.use(A) ← first added │
│ stack.use(B) ← second added │
│ stack.defer(C) ← third added │
│ │
│ On disposal: │
│ 1. C runs │
│ 2. B disposes │
│ 3. A disposes │
│ │
│ Last in, first out. Like a stack. │
│ │
└──────────────────────────────────────────────┘
The SuppressedError
┌──────────────────────────────────────────────┐
│ SUPPRESSED ERROR │
│ │
│ try { │
│ using resource = new Failing(); │
│ throw new Error("original"); │
│ } │
│ │
│ The try block throws "original". │
│ The disposal throws "disposal failed". │
│ │
│ Result: SuppressedError │
│ .error = "original" │
│ .suppressed = "disposal failed" │
│ │
│ Both errors preserved. Neither hidden. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Introduced | TypeScript 5.2 |
| Based on | TC39 Explicit Resource Management |
| Sync declaration | using x = ... |
| Async declaration | await using x = ... |
| Sync symbol | Symbol.dispose |
| Async symbol | Symbol.asyncDispose |
| Interfaces | Disposable, AsyncDisposable |
| Multi-resource | DisposableStack, AsyncDisposableStack |
| Error type | SuppressedError |
| Runtime support | V8 v13.8, Chromium 134 |
| Compile target | es2022 or below |
| Lib setting | esnext or esnext.disposable |
Key takeaways:
usingties resource lifetime to lexical scope. When the scope exits — by normal completion,return,throw, orbreak— the[Symbol.dispose]method is called. The disposal is guaranteed .await usinghandles asynchronous cleanup. It calls[Symbol.asyncDispose]and awaits the result. This is essential for database connections and network sockets that must close before the program continues .- The feature works with any object that implements the protocol. You don’t need a special base class. Just add
[Symbol.dispose]()or[Symbol.asyncDispose]()to your object . DisposableStackmanages multiple resources. It tracks resources and cleanup callbacks, disposing them in reverse order of addition. This is useful for conditional resources and ad-hoc cleanup .SuppressedErrorpreserves both errors. When the try block throws and disposal also throws, the original error is stored in.errorand the disposal error in.suppressed. Neither is lost .- Runtime support is required. The feature shipped in V8 v13.8 and Chromium 134. For older environments, polyfill
Symbol.disposeandSymbol.asyncDispose. - The TypeScript configuration needs
libandtarget. Set"lib": ["esnext"]or"esnext.disposable"and"target": "es2022"or below .
Remember: The using keyword is the JavaScript equivalent of RAII. It makes resource cleanup deterministic and exception-safe without the boilerplate of try/finally. Declare a resource with using, and the runtime guarantees its disposal when the scope exits. Use await using for asynchronous cleanup. Use DisposableStack when resources are conditional or when cleanup actions don’t map to a single object. The feature requires TypeScript 5.2+ and a runtime that supports the symbols — polyfill them if you’re targeting older environments. Once you adopt using, you will never write another manual close() call at the end of a function.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!