What you’ll do: Use JavaScript’s new using and await using declarations to clean up files, streams, and other resources automatically.

Who this is for: JavaScript developers who are tired of writing repetitive try...finally cleanup code.

Time: About 10 minutes.

Quick Answer

If your runtime supports explicit resource management, declare disposable values with using for synchronous cleanup or await using for asynchronous cleanup. When the scope ends, JavaScript automatically calls Symbol.dispose or Symbol.asyncDispose, which helps prevent leaks and trims a lot of boilerplate.

JavaScript finally has a cleaner way to handle resources that need cleanup. Instead of opening a file, stream, or lock and hoping you remembered the matching cleanup path, you can tie that cleanup to scope exit. The runtime handles the boring part, which is nice because boring parts are where bugs like to hide.

This trick is part of JavaScript’s explicit resource management work. The best starting points are the MDN using docs, the MDN await using docs, the V8 feature overview, and the Node.js FileHandle disposal docs.

What you need before you start

  • A recent JavaScript runtime or browser build with explicit resource management support. See the current compatibility notes in MDN and V8.
  • A code editor such as Visual Studio Code.
  • If you want typed support, TypeScript 5.2+ is a good baseline for the syntax.
  • A small test file or demo script where you can safely experiment.

Why this trick helps

Cleanup code is easy to skip during refactors, especially when a function opens more than one resource. Explicit resource management solves that by making lifetime visible in the code itself. According to MDN, cleanup runs when the surrounding scope exits, and V8 notes that disposers run in reverse order, which is exactly what you usually want when one resource depends on another.

Step 1: Check that your environment supports the feature

  1. Open the docs for your runtime or browser.
  2. Confirm support for using, await using, Symbol.dispose, and Symbol.asyncDispose.
  3. If you are using Node.js, start with the Node.js file handle example.

Expected check: You can identify a runtime version or docs page that explicitly lists the feature instead of guessing and hoping the parser is in a generous mood.

Step 2: Create a disposable object for synchronous cleanup

Start with a small object that implements Symbol.dispose:

class DemoResource {
  constructor(name) {
    this.name = name;
    console.log(`Acquired ${name}`);
  }

  [Symbol.dispose]() {
    console.log(`Disposed ${this.name}`);
  }
}

{
  using resource = new DemoResource('cache lock');
  console.log(`Working with ${resource.name}`);
}

Expected check: Your console should print the acquire message, then the work message, then the dispose message when the block ends.

Step 3: Replace a simple try...finally block

If you already have code like this:

const resource = openThing();
try {
  doWork(resource);
} finally {
  resource.close();
}

You can often rewrite it like this:

{
  using resource = openThing();
  doWork(resource);
}

Expected check: The code is shorter, and cleanup still happens at the end of the block.

Step 4: Use await using for async cleanup

For resources that need async disposal, switch to await using. Node.js file handles are a practical example:

import fs from 'node:fs/promises';

async function readFirstLine() {
  await using file = await fs.open('example.txt', 'r');
  const data = await file.readFile({ encoding: 'utf8' });
  return data.split('\n')[0];
}

MDN explains that await using awaits cleanup on scope exit, and Node documents FileHandle[Symbol.asyncDispose]() for this exact pattern.

Expected check: Your script reads the file successfully and closes the handle when the function exits.

Step 5: Keep scopes tight when you only need a resource briefly

The feature works best when the scope matches the actual lifetime of the resource:

async function processUpload() {
  {
    await using file = await openUploadTempFile();
    await validateFile(file);
  }

  await continueOtherWork();
}

Expected check: The temporary file is disposed before the rest of the function continues.

Step 6: Know when to reach for a stack instead

If your cleanup logic is more dynamic, look at DisposableStack and AsyncDisposableStack. V8 covers both in its feature write-up. They are useful when resources are created conditionally or you want a structured way to clean up several things together.

Expected check: You can explain why a simple block is enough for fixed lifetimes, while a stack is better for more flexible cleanup flows.

Common mistakes

  • Using the syntax in an unsupported runtime: parser errors are not subtle here.
  • Forgetting the disposal method: the object needs Symbol.dispose or Symbol.asyncDispose to behave like a disposable resource.
  • Making the scope too large: if the block is huge, the resource stays alive longer than necessary.
  • Using plain using for async cleanup: if cleanup must be awaited, use await using.
  • Assuming every library already supports it: some APIs still need a wrapper object for now.

Troubleshooting

You get a syntax error on using:
Check your runtime version and current support notes in MDN. If your environment is older, the feature may not be enabled yet.

Your object is not disposing:
Make sure it actually implements Symbol.dispose or Symbol.asyncDispose, and that your code exits the block or function where the variable was declared.

You need cleanup for several conditional resources:
Use AsyncDisposableStack or DisposableStack instead of forcing everything into one awkward block.

TypeScript accepts the code but runtime fails:
Type support and runtime support are separate. TypeScript 5.2 can understand the syntax, but your actual browser or Node.js version still has to support it.

Where this is most useful

  • Node.js file handles and streams
  • Database or cache wrappers with explicit cleanup
  • Locks, subscriptions, or readers that need release logic
  • Front-end platform code where resource lifetime is easy to lose track of

Try this next

Pick one real try...finally block from your codebase and rewrite it with using or await using. If the new version is shorter and easier to reason about, keep it. If not, at least you learned where the shiny new syntax actually helps instead of just looking clever in a demo.