Skip to content

Web Development · JavaScript

using and await using in JavaScript, Explained

ES2026's using and await using close files, connections, and locks when a block ends, with no try/finally. The syntax, real examples, and support today.

Anurag Verma

Anurag Verma

6 min read

using and await using in JavaScript

Sponsored

Share

Every backend developer has written this pattern dozens of times: open a resource, do something with it, close the resource in a finally block so it closes even if the code in between throws. It’s correct, and it’s also easy to get subtly wrong the moment a function needs two or three resources at once, because now you’re nesting try/finally blocks or writing a cleanup function that has to know about every resource acquired so far. ES2026’s using and await using declarations turn that pattern into language syntax: declare a resource with using, and its cleanup runs automatically when the block ends, no matter how it ends.

The problem in its old form

Here’s the pattern this feature replaces, using a hypothetical file handle:

function readConfig() {
  const handle = openFile('config.json');
  try {
    const data = handle.read();
    return JSON.parse(data);
  } finally {
    handle.close();
  }
}

One resource, one try/finally, fine. Now add a second resource, a lock that has to be released, and the nesting starts:

function updateConfig(newData) {
  const lock = acquireLock('config.lock');
  try {
    const handle = openFile('config.json', 'w');
    try {
      handle.write(JSON.stringify(newData));
    } finally {
      handle.close();
    }
  } finally {
    lock.release();
  }
}

Every additional resource adds a level of nesting, and the discipline required to always remember the finally block, in the right order, for every code path, is exactly the kind of thing that’s fine until someone’s in a hurry.

The same thing with using

function updateConfig(newData) {
  using lock = acquireLock('config.lock');
  using handle = openFile('config.json', 'w');
  handle.write(JSON.stringify(newData));
  // lock and handle are both released here, in reverse order,
  // whether this line is reached normally, via return, or via a throw above it
}

No nesting, no finally blocks, and the cleanup order is well-defined: resources dispose in the reverse order they were declared, the same way you’d unwind nested try/finally blocks by hand, except the language does it for you and can’t forget a step.

How a resource opts in: Symbol.dispose

using doesn’t work on arbitrary objects. A resource has to implement Symbol.dispose, a well-known symbol method, the same mechanism that lets an object work with for...of by implementing Symbol.iterator:

class FileHandle {
  #fd;
  constructor(fd) {
    this.#fd = fd;
  }
  read() {
    return readFileDescriptor(this.#fd);
  }
  [Symbol.dispose]() {
    closeFileDescriptor(this.#fd);
  }
}

function readConfig() {
  using handle = new FileHandle(openFd('config.json'));
  return JSON.parse(handle.read());
  // handle[Symbol.dispose]() runs automatically here
}

Any class, any object literal, anything that has a [Symbol.dispose]() method works with using. You don’t need a special base class or a library; it’s a protocol, not an inheritance hierarchy.

await using: for cleanup that needs to be awaited

Some resources can’t close synchronously, a database connection that needs a network round-trip to release cleanly, a file handle whose close operation is itself async. For those, implement Symbol.asyncDispose instead, and declare the resource with await using:

class DbConnection {
  #client;
  constructor(client) {
    this.#client = client;
  }
  query(sql) {
    return this.#client.query(sql);
  }
  async [Symbol.asyncDispose]() {
    await this.#client.release(); // returns the connection to the pool
  }
}

async function getUser(id) {
  await using db = new DbConnection(await pool.connect());
  const result = await db.query(`SELECT * FROM users WHERE id = ${id}`);
  return result.rows[0];
  // db[Symbol.asyncDispose]() is awaited here before the function returns
}

await using can only be used inside an async function, the same restriction as a bare await. The function doesn’t actually return until the cleanup’s promise resolves, which matters for a connection pool: the connection is genuinely back in the pool, not just “on its way back,” by the time the caller gets the result.

Managing several resources with DisposableStack

using handles one resource per declaration cleanly, but sometimes you’re accumulating a variable number of resources in a loop or a conditional, where you don’t know up front how many there will be. DisposableStack (and its async counterpart, AsyncDisposableStack) is built for that:

function processFiles(paths) {
  using stack = new DisposableStack();
  const handles = paths.map((path) => stack.use(new FileHandle(openFd(path))));

  return handles.map((h) => h.read());
  // every handle registered with stack.use() closes here,
  // in reverse registration order, even though the count wasn't known upfront
}

stack.use() registers a resource and returns it, so you can build the list of open handles inline. When the stack itself is disposed, it disposes everything registered with it, in reverse order, the same guarantee a single using declaration gives you, just generalized to an arbitrary count.

Where this actually helps, and where it doesn’t

This isn’t a replacement for try/finally in general; it solves one specific, extremely common shape of problem: a resource with exactly one cleanup step tied to a scope. Arbitrary error-handling logic, retries, or cleanup that depends on which branch of code ran still belongs in try/catch/finally. What using removes is the boilerplate and the risk of a forgotten or misordered cleanup call when the answer is genuinely “release this thing when we’re done with it, however we’re done.”

Node.js backend code is where this pays off fastest: connection pool checkouts, file handles, distributed locks, and any client library that wraps a network resource are exactly the objects worth adding Symbol.dispose or Symbol.asyncDispose to. If you’re building or maintaining an internal library that hands out resources callers are expected to release, implementing the dispose protocol on what you return costs a few lines and makes every caller’s code shorter and harder to get wrong. We’ve covered a related evergreen headache, connection lifecycle management, in more depth in our guide to database connection pooling in production, and using is a genuinely good fit for the checkout/release pattern described there.

What’s supported right now

This shipped as part of ES2026, not a stage-2 proposal anymore. As of 2026, using and await using work natively in Chrome 134+, Firefox 132+, Node.js 22+, and Deno, and TypeScript has supported the syntax since 5.2 with "lib": ["esnext.disposable"] in your tsconfig.json. If you need to support older Node versions or browsers, both Babel and TypeScript’s own downlevel compilation can transpile using to equivalent try/finally code, so there’s no reason to wait for 100% native support before adopting the syntax in a project that already transpiles.

Frequently asked questions

What problem does the using declaration solve?
The pattern of opening a resource (a file handle, a database connection, a lock) and needing to guarantee it closes even if the code in between throws or returns early. Before using, that meant a try/finally block around every single resource, and it was easy to forget one or nest them incorrectly when a function needed more than one resource at once.
How is using different from try/finally?
try/finally requires you to write the cleanup call yourself, every time, in the finally block. using calls Symbol.dispose() on the resource automatically when the block ends, for any reason, so there's no cleanup line to forget and no risk of a typo in the finally block leaving a resource open.
What is Symbol.dispose?
A well-known symbol method, similar to Symbol.iterator, that an object implements to define what its cleanup means. A file handle's Symbol.dispose might close the file descriptor; a database connection's might return it to a pool. using calls this method automatically when its block ends. Symbol.asyncDispose is the async equivalent for resources whose cleanup itself needs to be awaited, used with await using.
Do I need a library to use this?
No. using and await using are language syntax, part of ES2026, not a library feature. Any object that implements Symbol.dispose or Symbol.asyncDispose works with them directly. Some libraries are adding native disposable support to their own resource-returning functions, but the syntax itself works with any conforming object today.
Is this supported in Node.js and browsers right now?
Yes, broadly. Node.js 22 and later, Chrome 134+, Firefox 132+, and Deno all support it natively as of 2026, and TypeScript has supported the syntax since 5.2 with the esnext.disposable lib target. Older runtimes need a transpiler (Babel or TypeScript's own downlevel compilation) to use it.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored