using Declarations in TypeScript: Resource Management
AI generated
type
TypeScript
using Declarations: Explicit Resource Management
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.

10 min read TypeScript 5.2+ Runtime feature

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

11. FAQ: using Declarations

1What does a using declaration do in TypeScript?
It makes the compiler automatically call a resource's disposal method as soon as the surrounding block is left, whether normally or through an exception, which removes most manual try/finally blocks.
2Since which TypeScript version do using and await using exist?
Both were introduced in TypeScript 5.2 in August 2023, based on the TC39 proposal for Explicit Resource Management, which is also landing directly in JavaScript itself over time.
3What protocol must a resource implement to support using?
It must provide a method under the well-known symbol Symbol.dispose, matching the Disposable interface. Asynchronous disposal uses Symbol.asyncDispose instead.
4When should await using be used instead of using?
When the resource's own disposal is asynchronous, for instance because outstanding database requests must be awaited. The resource implements Symbol.asyncDispose instead of Symbol.dispose, and the compiler inserts an await automatically.
5In what order are multiple resources declared with using released?
In reverse declaration order, following the LIFO principle, similar to nested try/finally blocks. This matters especially when resources depend on each other.
6Does using replace try/catch error handling?
No, using only removes the need for try/finally for cleanup. Actual error handling still uses try/catch, and both combine without any friction.
7What happens if both the code and the disposal method throw an error?
Both errors are merged by the runtime into a single AggregateError, so neither piece of error information gets lost and both remain visible in the catch block.
8What is DisposableStack and when is it needed?
DisposableStack collects a dynamic number of resources at runtime, for instance inside a loop, and releases them together in the correct order when a using block is left, without manual bookkeeping.
9What runtime requirements does using have?
The compilation target must be at least ES2022, or the matching symbol definitions must be included via the lib option. Older runtime environments additionally need a polyfill for Symbol.dispose.
10For which kinds of resources is using especially useful?
File handles, network connections, database transactions, locks in concurrent systems, and test code that needs to reliably reset temporary environments or mocks, as well as frontend event listeners.