Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
JavaScriptTutorial

JavaScript using declarations: automatic cleanup without finally

Learn JavaScript's using and await using declarations, Symbol.dispose and DisposableStack, with tested browser recipes and October 2026 support data.

Cloudsoft Zone

8 min read

Out of Hours Returns book drop slot set into the glass front of Woking Library
Photograph: Zorro2212 / CC BY-SA 3.0, via Wikimedia Commons
In this article

JavaScript's using declaration runs a cleanup method automatically when a block ends, whether it ends normally, with return, or with an exception. You give an object a [Symbol.dispose]() method, declare it with using instead of const, and the language calls that method for you. It replaces many try...finally blocks, and it works today in Chrome, Edge and Firefox, but not yet in a released version of Safari.

This tutorial shows how the feature behaves, how to write disposable objects for everyday browser code (event listeners, timers, observers and abortable work), when to reach for DisposableStack, and how to adopt it safely while Safari catches up. Every example was run on Node.js 24, which ships the same V8 implementation as Chrome.

Why cleanup code goes wrong

Front-end code acquires plenty of things that must be released: event listeners, intervals, MutationObserver and ResizeObserver instances, AbortControllers, object URLs, locks and stream readers. The traditional pattern is try...finally:

JavaScript
const id = setInterval(poll, 5000);
try {
  await runWizard();
} finally {
  clearInterval(id);
}

That works for one resource. With three, you nest three try blocks, or you write one finally that has to know which resources were actually acquired before the error. Cleanup also ends up far away from acquisition, so it is easy to forget when someone edits the code later. using keeps acquisition and cleanup on the same line.

The basics: Symbol.dispose and using

An object is disposable when it has a method under the well-known symbol Symbol.dispose. Here is a small wrapper that removes an event listener when it goes out of scope:

JavaScript
class TempListener {
  constructor(target, type, handler) {
    this.target = target;
    this.type = type;
    this.handler = handler;
    target.addEventListener(type, handler);
  }
  [Symbol.dispose]() {
    this.target.removeEventListener(this.type, this.handler);
  }
}

const target = new EventTarget();
{
  using listener = new TempListener(target, 'ping', () => console.log('ping received'));
  target.dispatchEvent(new Event('ping')); // "ping received"
} // listener removed here

target.dispatchEvent(new Event('ping')); // nothing happens

A using variable behaves like const: it must be initialised, cannot be reassigned and is block-scoped. The rules MDN documents are worth knowing before you start:

  • Reverse order. With several declarations in one scope, disposers run in the reverse order of declaration, like unwinding a stack.
  • Always runs. Disposal happens on every exit path, exactly like a finally block.
  • Null is allowed. The value may be null or undefined, which makes optional resources easy. Any other value without a callable [Symbol.dispose] throws a TypeError at the declaration.
  • No destructuring. using { a, b } = obj is a syntax error; declare the whole object.
  • Where it is allowed. Inside blocks, function bodies, class static blocks, at the top level of a module and in for, for...of and for await...of initialisers. It is not allowed at the top level of a classic script, at the top level of a switch statement, or in a for...in loop.
JavaScript
const res = (name) => ({ [Symbol.dispose]() { console.log(`dispose ${name}`); } });
{
  using a = res('a');
  using b = res('b');
  console.log('body');
}
// body
// dispose b
// dispose a
Row of grey underground recycling bins beside a pavement in front of a brick building in Drammen
Each disposable object knows how to clean up after itself; using decides when that happens.

Recipes for browser code

A timer that cannot leak

JavaScript
function timeout(ms, callback) {
  const id = setTimeout(callback, ms);
  return { [Symbol.dispose]() { clearTimeout(id); } };
}

function showToastWhileSaving(save) {
  using slowHint = timeout(800, () => showToast('Still saving...'));
  return save(); // fast saves never show the toast
}

If save() throws synchronously, the timer is still cleared. Note that for an async save() you need to await inside the block; otherwise the block ends as soon as the promise is returned.

Observers that disconnect themselves

JavaScript
function observe(element, callback) {
  const observer = new ResizeObserver(callback);
  observer.observe(element);
  return { [Symbol.dispose]: () => observer.disconnect() };
}

Loops where each item owns a resource

JavaScript
for (using item of openItems()) {
  process(item);
} // each item is disposed at the end of its own iteration

await using for asynchronous cleanup

Some cleanup is asynchronous: flushing a write queue, closing a connection, releasing a lock held by a server. For those, implement [Symbol.asyncDispose]() and declare the variable with await using inside an async function or a module:

JavaScript
async function openSession() {
  const session = await api.startSession();
  return {
    session,
    async [Symbol.asyncDispose]() {
      await api.endSession(session.id);
    },
  };
}

async function exportReport() {
  await using s = await openSession();
  return await s.session.download('report.csv');
} // endSession() is awaited before exportReport() settles

Two details from MDN catch people out. First, await using falls back to [Symbol.dispose] when there is no async disposer, but a promise returned by that sync method is not awaited. Second, a scope with an await using declaration always awaits at least once on exit, even when the value is null, so the timing of the function does not change depending on whether the resource existed.

DisposableStack: many resources, one owner

using covers resources you declare one by one. When you acquire a dynamic number of them, or need to hand ownership to someone else, use DisposableStack (or AsyncDisposableStack):

MethodUse it for
use(value)Register an object that already has [Symbol.dispose].
adopt(value, onDispose)Register something that is not disposable, with your own cleanup function.
defer(onDispose)Register a plain callback.
move()Transfer every registered disposer to a new stack and mark this one as disposed.
dispose()Run all disposers in reverse order of registration.

The classic case is a setup function: if step three fails, steps one and two must be undone, but if everything succeeds the caller becomes responsible for cleanup.

JavaScript
function mountWidget(element) {
  using stack = new DisposableStack();

  const controller = stack.adopt(new AbortController(), (c) => c.abort());
  element.addEventListener('click', onClick, { signal: controller.signal });
  window.addEventListener('resize', onResize, { signal: controller.signal });

  const url = URL.createObjectURL(new Blob(['...']));
  stack.defer(() => URL.revokeObjectURL(url));

  renderInto(element, url); // if this throws, everything above is undone

  return stack.move(); // success: hand ownership to the caller
}

const widget = mountWidget(document.querySelector('#chart'));
// later, for example when the user navigates away:
widget.dispose();

Because move() leaves the original stack marked as disposed, the using stack declaration does nothing on the success path. MDN also recommends wrapping the acquisition expression directly in use() or adopt(), as above, so no code can run between acquiring a resource and registering it.

Hotel room door handle with an electronic lock and a key card inserted in its reader slot
DisposableStack.move() hands ownership over, much like passing a room key to the next person responsible.

When cleanup itself fails: SuppressedError

If the block throws and a disposer also throws, you would lose one of the two errors with a hand-written finally. using keeps both in a SuppressedError: the earlier error goes in suppressed, the later one in error.

JavaScript
try {
  using r = { [Symbol.dispose]() { throw new Error('cleanup failed'); } };
  throw new Error('work failed');
} catch (e) {
  console.log(e.name);              // "SuppressedError"
  console.log(e.error.message);     // "cleanup failed"
  console.log(e.suppressed.message); // "work failed"
}

Make sure your error reporting reads both properties; a logger that only prints message will show "An error was suppressed during disposal" and hide the real cause.

Browser support and adopting it today

According to MDN's browser compatibility data (version 8.1.5, 8 October 2026), using, await using, DisposableStack, AsyncDisposableStack and SuppressedError are supported from:

EngineFirst version
Chrome and Edge134
Firefox141
Safari (macOS)Technology Preview only
Safari on iOSNot supported
Node.js24.0.0

On the standards side, TC39's list of finished proposals includes Explicit Resource Management with an expected publication year of 2027, so the semantics are settled even though one engine has not shipped yet.

The Symbol.dispose protocol and DisposableStack are ordinary built-ins and can be polyfilled. The using keyword is new syntax: a browser that does not understand it rejects the whole script file, so a polyfill cannot help. Your options are:

  1. Write disposable objects now, call them manually. Implement [Symbol.dispose]() on your helpers (guarding with Symbol.dispose ??= Symbol('Symbol.dispose') or a polyfill) and call obj[Symbol.dispose]() from finally. You can switch to using later without changing the helpers.
  2. Compile the syntax. TypeScript and Babel can transform using into try...finally code for older targets.
  3. Use it natively where you control the runtime, for example in Chrome-only internal tools, browser extensions for Chromium and Firefox, or Node.js 24 build scripts.

Gotchas

  • Closures outlive the block. A callback that captures a using variable may run after disposal and work with a closed resource. Pass ownership with DisposableStack.move() instead.
  • Do not export disposed values. export using is a syntax error, and exporting a module-level using variable gives importers a resource that is already disposed.
  • Forgetting await in the block. using disposes when the block ends, not when a promise you returned settles.
  • Classic scripts. Top-level using works only in modules (<script type="module">).

What to do next

  • Pick one helper that has a matching "stop" or "close" function (a subscription, a timer, an observer) and give it a [Symbol.dispose]() method.
  • Replace a multi-resource setup function with DisposableStack and move().
  • Check your analytics for Safari share before shipping uncompiled using syntax to the public web.

The payoff is code where every acquisition visibly owns its cleanup, and where a failure halfway through setup no longer leaves listeners, timers and object URLs behind.

Frequently asked questions

  • Is using the same as const?

    It has the same scoping and reassignment rules as const, plus one extra behaviour: when the block ends, the value's [Symbol.dispose]() method is called. The value must be null, undefined or an object with that method.

  • Which browsers support using in October 2026?

    Chrome and Edge 134 and later, and Firefox 141 and later, according to MDN's compatibility data. Safari supports it only in Technology Preview, and Safari on iOS does not support it.

  • Can I polyfill the using keyword?

    No. It is syntax, so older engines fail to parse the file. You can polyfill Symbol.dispose and DisposableStack, and compile using with TypeScript or Babel.

  • What happens if a dispose method throws?

    All disposers still run. If more than one error occurs, they are combined into a SuppressedError whose error property holds the later error and whose suppressed property holds the earlier one.

  • When should I use await using instead of using?

    When cleanup is asynchronous and must finish before the function continues, such as closing a session on a server. Implement [Symbol.asyncDispose]() and declare the variable with await using inside an async function or a module.

  • Can I use using at the top level of a normal script tag?

    No. Top-level using is only allowed in modules. Inside a classic script, put it in a block or a function body.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. using — MDN Web Docs
  2. await using — MDN Web Docs
  3. DisposableStack — MDN Web Docs
  4. ECMAScript Explicit Resource Management proposal — TC39
  5. Finished Proposals — TC39
  6. MDN browser-compat-data 8.1.5 (data.json) — MDN Web Docs, 2026-10-08

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.