Diego Betto's Blog
Photo by Jan Tinneberg on Unsplash

Diego Betto · September 15, 2026 · 4 min di lettura

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.

Condividi:XLinkedInFacebookWhatsApp

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

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

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