Deterministic resource cleanup without try/finally boilerplate
TypeScript 5.2 introduced using and await using, based on the TC39 proposal for Explicit Resource Management. Resources such as file handles, database connections, or locks are now released automatically and deterministically as soon as their scope is left.
Table of Contents
- 1. The Problem With Manual Cleanup
- 2. Basics: the Symbol.dispose Protocol
- 3. await using for Asynchronous Resources
- 4. Disposal Order With Multiple Resources
- 5. Combining With try/catch
- 6. DisposableStack for Dynamic Resource Lists
- 7. How This Compares to Other Languages
- 8. Typical Use Cases
- 9. Pitfalls and Runtime Requirements
- 10. Summary
- 11. FAQ
1. The Problem With Manual Cleanup
Resources that must be explicitly released, such as open files, network connections, database transactions, or locks, traditionally require a try/finally construct so the release still happens reliably even when an exception is thrown.
With several resources inside one function, this pattern quickly becomes unwieldy, since every additional resource adds another level of nested try/finally blocks just to guarantee the correct release order.
using declarations solve this problem by having the compiler automatically generate code that calls the resource's disposal method as soon as the surrounding block is left, whether normally or through an exception.
2. Basics: the Symbol.dispose Protocol
A resource must follow the disposable protocol, meaning it provides a method under the well-known symbol Symbol.dispose. When such a resource is declared with using instead of const, the compiler automatically calls this method at the end of the block.
The example below shows a simple resource class and its usage with using. The console output demonstrates that disposal happens exactly when the block is left, not at the end of the function or not at all.
It's worth noting that using isn't restricted to a specific class of resources: any object that provides a method under Symbol.dispose can be declared with using, whether it's a class you wrote yourself or an instance from a third-party library.
class FileHandle implements Disposable {
constructor(private path: string) {
console.log(`open: ${this.path}`);
}
[Symbol.dispose]() {
console.log(`close: ${this.path}`);
}
}
function readConfig() {
using handle = new FileHandle("config.json");
// handle's dispose() is called automatically when the block ends
}
readConfig();
// output: open: config.json
// output: close: config.json
3. await using for Asynchronous Resources
For resources whose release is itself asynchronous, for example a database connection that must wait for pending requests before closing, there's the await using variant. It expects a method under Symbol.asyncDispose that returns a promise.
The compiler automatically inserts an await at the right spot, so the resource is fully closed before the surrounding async context continues.
A class can implement both Symbol.dispose and Symbol.asyncDispose at the same time, so it can be used with either using or await using, with the synchronous variant usually representing a simplified, immediate disposal with no waiting involved.
class DbConnection implements AsyncDisposable {
async [Symbol.asyncDispose]() {
await this.flushPendingQueries();
console.log("connection closed");
}
private async flushPendingQueries() {
// waits for outstanding requests
}
}
async function query() {
await using db = new DbConnection();
// connection is closed asynchronously after leaving the block
}
4. Disposal Order With Multiple Resources
When several resources are declared with using inside the same block, the compiler releases them in reverse declaration order, following the LIFO principle, much like a stack. That matches the intuitive behavior of nested try/finally blocks, while completely avoiding the manual nesting.
This order matters especially when resources depend on each other, for example a transaction that must be closed before the underlying connection.
function work() {
using a = openResource("A");
using b = openResource("B");
using c = openResource("C");
// disposal happens in the order C, B, A
}
5. Combining With try/catch
using declarations replace try/finally for cleanup, but they don't replace try/catch for error handling. Both combine without friction: disposal is guaranteed regardless of whether a catch block handles the error or the exception propagates further up.
If both the main code and the disposal method itself throw an error, the runtime merges both into a single AggregateError, so no error information gets silently lost.
function process() {
try {
using res = acquireResource();
riskyOperation();
} catch (err) {
console.error("error handled:", err);
}
}
6. DisposableStack for Dynamic Resource Lists
Sometimes the number of resources is only known at runtime, for example in a loop that dynamically opens several connections. For this case, the standard library provides DisposableStack and AsyncDisposableStack, to which resources can be added dynamically.
The stack itself also implements Symbol.dispose, so it can be used together with a using declaration and, upon leaving the block, releases every contained resource in the correct order.
function openAll(paths: string[]) {
using stack = new DisposableStack();
const handles = paths.map((p) => stack.use(new FileHandle(p)));
return handles.length;
// every handle is closed automatically here
}
7. How This Compares to Other Languages
The pattern behind using has long been established in other languages. C# has had a using statement with almost identical semantics for years, and Python solves the same problem through the with context manager and the __exit__ protocol. TypeScript deliberately adopts a comparable but JavaScript-idiomatic concept with Symbol.dispose, built on well-known symbols rather than a dedicated language keyword like in C#.
The key difference from these role models is that Symbol.dispose is part of the TC39 proposal and therefore lands directly in the JavaScript runtime itself over time, not just as a TypeScript-specific compiler feature. Node.js and modern browser engines already support the protocol natively, so code generated by TypeScript runs in current environments without any extra library.
For teams moving to TypeScript from object-oriented languages, this similarity makes the transition considerably easier, since the mental model of resource lifecycles carries over directly without having to learn an entirely new paradigm.
8. Typical Use Cases
using is a good fit for any resource with a clear lifecycle: file handles, network sockets, database connections and transactions, locks in concurrent systems, and performance measurements where a timer should stop and log automatically once a block is left.
The pattern is also valuable in test code, for example to reliably reset temporary test environments or mocks without maintaining a separate afterEach hook in every test.
In frontend applications, using is also well suited for cleaning up event listeners, observers, or subscriptions, which are otherwise easily forgotten and lead to memory leaks when a component is removed while its registration stays active.
9. Pitfalls and Runtime Requirements
using and await using are both a language feature and a runtime feature: the compiler generates calls to Symbol.dispose or Symbol.asyncDispose that must actually exist at runtime. In environments without native support for these well-known symbols, a polyfill is required.
The compilation target must also be at least ES2022, or the corresponding symbol definitions must be included via the lib compiler option. Using using in a project with an older target reports a compiler error due to the missing type definitions for Symbol.dispose.
Another often overlooked point: using declarations are block scoped like let and const, not function scoped. If a resource is declared with using inside a loop, disposal happens again on every iteration, which can lead to more frequent opening and closing than actually intended if used carelessly.
Finally, keep in mind that a using variable cannot be reassigned, just like const. Anyone wanting to swap out a resource within the same block needs a new block or a dedicated helper function instead.
| Aspect | try/finally | using | await using |
|---|---|---|---|
| Automatic disposal | Manual, in finally | Automatic | Automatic, asynchronous |
| Protocol | No fixed protocol | Symbol.dispose | Symbol.asyncDispose |
| Multiple resources | Manual nesting required | Automatic LIFO | Automatic LIFO |
| Error aggregation | Manual | Automatic AggregateError | Automatic AggregateError |
| Minimum version | All versions | TypeScript 5.2+ | TypeScript 5.2+ |
Mironsoft
TypeScript migration, type safety, and team onboarding
A JavaScript codebase without type safety, but no time for a full migration?
We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.
Migration Roadmap
Plan and execute a gradual JS-to-TS migration without big-bang risk.
Strict Mode Rollout
Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.
Team Onboarding
Bring developers up to speed on TypeScript best practices with workshops and reviews.
10. Summary
using Declarations
Available since
TypeScript 5.2
Based on
TC39 Explicit Resource Management
Disposal order
LIFO, like a stack
Typical use
Files, connections, locks