completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Explicit resource management

Learn using, await using, DisposableStack, and SuppressedError to clean up files, locks, listeners, timers, and object URLs reliably.

By the end, you can
  • 01
    Clean up resources at a known momentCreate variables with using so their [Symbol.dispose]() method runs however the block ends.
  • 02
    Wait for cleanup that takes timeUse await using and [Symbol.asyncDispose]() when closing a resource returns a promise.
  • 03
    Group cleanup and read cleanup errorsUse DisposableStack and AsyncDisposableStack, and read a SuppressedError without losing the first error.

Why cleanup needs help

JavaScript frees memory for you. When no code can reach an object any more, the garbage collector removes it. You don't know when that happens.

Some things need more than freed memory. An open file must be closed. A lock must be released, so other code can take it. A click listener must be removed. The garbage collector does none of this at the moment you are done.

These things are called resources. Your code holds a small object for each one, called a handle. The handle lets you use the resource and then give it back. Giving it back is called disposing of it, or cleanup.

The usual tool for cleanup is try/finally. The code in finally runs however the try block ends. It works, but with several resources the cleanup code gets long and easy to get wrong.

Definition

Explicit resource management is a JavaScript feature that cleans up resources at a known moment. You write using instead of const to create the variable. JavaScript then ties the resource to its scope: the block of code between { and } where the variable exists. When the code leaves that block, JavaScript calls the resource's cleanup method, [Symbol.dispose](). For cleanup that takes time, you write await using, and JavaScript waits for [Symbol.asyncDispose]() to finish.

Three ways a block can end: a normal end, a return, or a throw. All three arrows lead to the same cleanup method, [Symbol.dispose]().how the block endsnormal endreturnthrowJavaScript calls[Symbol.dispose]()
However the block ends, JavaScript calls the resource's [Symbol.dispose]() method on the way out. A normal end, a return, and a throw all lead there.

These are common resources that need cleanup:

  • open files
  • locks
  • database connections
  • subscriptions to updates
  • timers
  • short-lived event listeners
  • blob URLs made with URL.createObjectURL()
Real-life analogyA checkout desk for borrowed equipment

You borrow equipment from a photo studio. First you pick up a camera. Then a tripod. Then a visitor badge. When you leave the building, you return everything at the desk. The badge goes back first, then the tripod, then the camera. The studio has only a few of each, so they must come back on time.

In real life: Borrow a camera, a tripod, and a badge
In JavaScript: Open a file, take a lock, and add a listener
In real life: Leave the building
In JavaScript: Leave the block where the variables exist
In real life: Return the newest item first
In JavaScript: Clean up the last resource first
Finally still runs after a throwPop out in the code editor (opens in a new tab)JavaScript
const events = [];
try {
  events.push("work");
  throw new Error("failed");
} catch (error) {
  events.push(error.message);
} finally {
  events.push("close");
}
console.log(events.join(" -> "));

Line 3 records work. Line 4 throws an error. The catch block records the message, failed. Then the finally block records close. The output is work -> failed -> close.

try/finally is still useful. Use it in older browsers, or for cleanup that doesn't belong to one object.

Manual cleanup with try/finallyPop out in the code editor (opens in a new tab)JavaScript
const events = [];let file;let lock; function open(name) {  events.push(`open ${name}`);  return { close: () => events.push(`close ${name}`) };} try {  file = open("file");  lock = open("lock");  events.push("work");} finally {  if (lock) lock.close();  if (file) file.close();} console.log(events.join(" -> "));

Line 11 opens the file. Line 12 opens the lock. Lines 15 and 16 close them again, in the opposite order: the lock first, then the file. You must remember to write both lines. Line 19 prints open file -> open lock -> work -> close lock -> close file.

Add a third resource, an if, or an early return, and the cleanup code starts to hide the real work. There is one more problem. If lock.close() throws, line 16 never runs, so the file stays open. To be sure both close, you need one try/finally inside another.

A failed second acquisitionPop out in the code editor (opens in a new tab)JavaScript
const events = [];
function openSecond() { throw new Error("second open failed"); }
try {
  using first = { [Symbol.dispose]() { events.push("close first"); } };
  using second = openSecond();
} catch (error) {
  events.push(error.message);
}
console.log(events.join(" -> "));

Line 4 creates first with using, so JavaScript already knows to clean it up. Line 5 tries to open a second resource, but openSecond() throws. JavaScript cleans up first before the catch runs. The output is close first -> second open failed.

The lesson: hand each resource to using as soon as you get it. Don't wait until you have all of them.

The table compares four cleanup tools. Read each row as: what the tool is good for, and what it costs.

Choosing a cleanup tool
ToolWhen it fits bestWhat to watch out for
try/finallyIt works everywhere and can do any kind of cleanup.With several resources it gets long. You must write the reverse order yourself, and a missing finally leaks.
usingOne or more resources that belong to one block.The runtime must understand using, or TypeScript must rewrite it. The value must have a [Symbol.dispose]() method.
DisposableStackResources you get in different places, or only sometimes.Cleanup still runs at a known moment. But you must add each cleanup to the stack, and clean up the stack itself.
FinalizationRegistryA backup message after the garbage collector removes an object.It may run late or never. It can't replace closing files, locks, sockets, blob URLs, or listeners.

using and Symbol.dispose

STEP THROUGH

An object can be cleaned up by using when it has a method named [Symbol.dispose]. Symbol.dispose is a built-in symbol: a special, unique property key that JavaScript itself knows about. The square brackets mean "use this symbol as the method name".

The line using file = openFile() does two things:

  • It creates the variable file, which you can use inside the block.
  • It tells JavaScript to call file[Symbol.dispose]() when the code leaves the block.

The cleanup runs even if the block throws an error or returns early. With two using lines, the one written last is cleaned up first. Step through the code below. Pick each of the three ways out, and watch the order of the events.

Editor support

Edit & run opens the code in a separate browser sandbox. These examples need a browser that understands using: Chrome 134+, Edge 134+, or Firefox 141+. Safari's normal release and Node 22 stop with an error before any code runs. The feature check later in this lesson tests this page, not the editor sandbox. If your browser doesn't support using, read the try/finally examples, or let TypeScript rewrite the code for you.

A missing lock needs no ifPop out in the code editor (opens in a new tab)JavaScript
const events = [];
{
  using maybeLock = null;
  using lock = { [Symbol.dispose]() { events.push("unlock"); } };
  events.push("work");
}
console.log(events.join(" -> "));

Line 3 sets maybeLock to null, so there is nothing to clean up. JavaScript simply skips it. Line 4 creates a real lock. When the block ends, the lock is cleaned up, so the code prints work -> unlock.

So a resource that might be missing is fine. A normal object with no cleanup method is not; you'll see why below.

Step through: the order of cleanup with using
Step 0 of 14Ready
Your turn: follow the blue line

Step through the end of the block. Change how it ends to see that cleanup runs after a normal end, an early return, and an error.

Running in
  1. script
Next: line 1
 function resource(name) {  events.push(`acquire ${name}`);  return {    [Symbol.dispose]() {      events.push(`dispose ${name}`);    },  };} function work() {  using file = resource("file");  using lock = resource("lock");  events.push("work");  if ("normal" === "throw") throw new Error("write failed");  if ("normal" === "return") return "returned early";  events.push("done");  return "finished";} try {  console.log(work());} catch (error) {  console.log(error.message);}console.log(events.join(" -> "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the block ends

The cleanup order stays the same each time. Only the result changes: a normal result, an early return value, or an error.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Two boxes, file and lock. The top arrow goes from file to lock: file is acquired first, then lock. The bottom arrow goes from lock back to file: lock closes first, then file.filelock1. acquire2. disposecloses lastcloses first
file is acquired first, then lock. On the way out the order flips: lock closes first, and file closes last.
What using accepts

null and undefined are allowed. JavaScript skips them. Any other value must have a [Symbol.dispose] method. A plain object without one causes a TypeError on the using line itself.

Null is skipped; a plain object failsPop out in the code editor (opens in a new tab)JavaScript
{  using nothing = null;  using alsoNothing = undefined;  using handle = {    [Symbol.dispose]() {      console.log("disposed handle");    },  };} try {  {    using broken = {};  }} catch (error) {  console.log(error instanceof TypeError);  console.log(error.name);}

Lines 2 and 3 are allowed and do nothing. Line 4 creates an object with a cleanup method, so it prints disposed handle when the block ends.

Line 13 is the mistake. {} is not null or undefined, and it has no cleanup method. JavaScript throws a TypeError instead of guessing. Lines 16 and 17 print true and TypeError.

A saved copy is already closedPop out in the code editor (opens in a new tab)JavaScript
let saved;
{
  using handle = {
    closed: false,
    [Symbol.dispose]() { this.closed = true; },
  };
  saved = handle;
}
console.log(saved.closed);

Line 7 saves the handle in saved, a variable outside the block. That copies the reference, but the block still owns the resource. The block closes the handle when it ends, so line 9 prints true.

Don't return or save a handle from a using block. Return the data you read from it instead.

await using waits for cleanup that takes time

ASYNC

Some cleanup takes time. Closing a database connection, saving the last data in a stream, or undoing a half-finished database change all return a promise: an object that finishes later.

For these resources, give the object an [Symbol.asyncDispose]() method and create the variable with await using. The block does not finish until each of these cleanup promises has finished.

await using also accepts Symbol.disposePop out in the code editor (opens in a new tab)JavaScript
(async () => {
  const events = [];
  {
    await using lock = { [Symbol.dispose]() { events.push("unlock"); } };
    events.push("work");
  }
  console.log(events.join(" -> "));
})();

The lock on line 4 has only [Symbol.dispose], the normal cleanup method. That's fine: when there is no [Symbol.asyncDispose], await using uses [Symbol.dispose] instead. It still pauses for a moment when the block ends. The output is work -> unlock.

The other way round doesn't work. A plain using can't use an object that only has [Symbol.asyncDispose].

Step through: await using waits for cleanup
Step 0 of 12Ready
Your turn: follow the blue line

await using is for resources whose cleanup returns a promise. Step through and watch JavaScript wait for each cleanup, newest first.

Running in
  1. script
Next: line 2
(async () => {  function connection(name) {    events.push(`open ${name}`);    return {      async [Symbol.asyncDispose]() {        events.push(`start close ${name}`);        await Promise.resolve();        events.push(`closed ${name}`);      },    };  }   {    await using db = connection("db");    await using stream = connection("stream");    events.push("query");  }   console.log(events.join(" -> "));})();
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The last log on line 20 is outside the block on purpose. It runs only after the stream has closed and then the database has closed. The order is still reversed: the stream was opened last, so it closes first.

Wait to open, and wait to closePop out in the code editor (opens in a new tab)JavaScript
(async () => {
  const events = [];
  async function open() {
    events.push("open");
    return { async [Symbol.asyncDispose]() { events.push("close"); } };
  }
  {
    await using connection = await open();
    events.push("work");
  }
  console.log(events.join(" -> "));
})();

Line 8 has two waits, at two different times:

  • await open() waits right away, until the connection is open.
  • await using waits later, at the end of the block, until the cleanup is done.

The output is open -> work -> close.

A new resource for each loop turnPop out in the code editor (opens in a new tab)JavaScript
const events = []; function* rows() {  yield {    name: "A",    [Symbol.dispose]() {      events.push("close A");    },  };  yield {    name: "B",    [Symbol.dispose]() {      events.push("close B");    },  };} for (using row of rows()) {  events.push(`read ${row.name}`);} console.log(events.join(" -> "));

Line 18 is for (using row of rows()). Each turn of the loop gets a new row variable, created with using. The row is cleaned up at the end of its turn, before the loop reads the next row. Line 22 prints read A -> close A -> read B -> close B.

This helps when only one resource may be open at a time, such as one page of database results.

DisposableStack and AsyncDisposableStack

STACKS

Sometimes a block gets its resources from different places. One helper function creates a URL. Another takes a lock. A third adds a callback, but only if a setting is on. Writing a using line for each one is hard.

A DisposableStack collects all the cleanup in one place. Each time you get a resource, you add its cleanup to the stack. At the end, you clean up the stack once, and it cleans up everything in it.

The stack must be cleaned up tooPop out in the code editor (opens in a new tab)JavaScript
const events = [];
{
  using stack = new DisposableStack();
  stack.defer(() => events.push("remove listener"));
  events.push("listening");
}
console.log(events.join(" -> "));

Line 3 creates the stack with using, so the stack is cleaned up when the block ends. Line 4 adds one cleanup callback right away. The output is listening -> remove listener.

A stack does not clean itself up. Create it with using, or call its dispose() method yourself.

Step through: a DisposableStack collects cleanup
Step 0 of 10Ready
Your turn: follow the blue line

Step through a stack that uses defer, use, adopt, move, dispose, and disposed.

Running in
  1. script
Next: line 1
const stack = new DisposableStack();stack.defer(() => events.push("defer cache"));stack.use({  [Symbol.dispose]() { events.push("close file"); },});stack.adopt("lock", (name) => events.push(`release ${name}`));const moved = stack.move();console.log(stack.disposed);moved.dispose();console.log(moved.disposed);console.log(events.join(" -> "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
A pile of three cleanup cards. defer cache was added first and sits at the bottom. close file was added second. release lock was added third and sits on top. dispose() takes them from the top: release lock runs first, defer cache runs last.release lockclose filedefer cacheadded 3rdadded 2ndadded 1struns 1struns 2ndruns 3rddispose()
Each new cleanup goes on top of the pile. dispose() takes from the top, so the last one added runs first.

A stack has a few methods and one property:

  • use(value) adds an object that already has a [Symbol.dispose] method.
  • adopt(value, onDispose) adds a plain value, plus a function that cleans it up.
  • defer(fn) adds a cleanup function on its own.
  • move() moves all the cleanup to a new stack, and marks the old stack as done.
  • dispose() runs all the cleanup, last added first.
  • disposed is true once the stack has been cleaned up.
Disposing twice is safePop out in the code editor (opens in a new tab)JavaScript
const stack = new DisposableStack();
let closes = 0;
stack.defer(() => closes++);
stack.dispose();
stack.dispose();
console.log(closes, stack.disposed);

Lines 4 and 5 both call dispose(), but the callback runs only once. The second call does nothing. Line 6 prints 1 true.

When you open a new handle, add it to the stack in the same line: stack.use(open()). Then no code can run between opening the handle and adding it.

AsyncDisposableStack works the same wayPop out in the code editor (opens in a new tab)JavaScript
(async () => {  const events = [];  {    await using stack = new AsyncDisposableStack();    stack.defer(async () => events.push("defer metrics"));    stack.use({      async [Symbol.asyncDispose]() { events.push("close socket"); },    });    stack.adopt("transaction", async (value) => events.push(`rollback ${value}`));    events.push("inside");  }  console.log(events.join(" -> "));})();

Use AsyncDisposableStack when some cleanup returns a promise. Create it with await using on line 4, so JavaScript waits for the whole stack when the block ends. Lines 5, 6, and 9 add three cleanups. Line 10 records inside. When the block ends, the three cleanups run in reverse order. Line 12 prints inside -> rollback transaction -> close socket -> defer metrics.

Which kind of cleanup is it?
  • `using lock = acquireLock()`
  • `await using db = await connect()`
  • `stack.defer(() => revoke(url))`
  • `new FinalizationRegistry(cleanup)`
  • `finally { file.close(); }`
  • `stack.use(fileHandle)`
Try it yourself
0 of 6 correct

Sort each card by how its cleanup is run. 'Cleanup at a known moment' includes try/finally and a direct using. 'Cleanup through a stack' means a DisposableStack holds the cleanup.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

SuppressedError: when cleanup fails too

REAL OUTPUT

Cleanup can fail too. Closing a file can throw an error. Undoing a database change can fail. Removing a listener can hit a browser error.

Sometimes the block is already ending because of an error, and then the cleanup throws a second error. JavaScript doesn't drop either one. It throws a new error, a SuppressedError, that holds both.

A failed cleanup keeps the first error tooPop out in the code editor (opens in a new tab)JavaScript
try {  {    using handle = {      [Symbol.dispose]() {        throw new Error("cleanup failed");      },    };    throw new Error("work failed");  }} catch (error) {  console.log(error instanceof SuppressedError);  console.log(error.error.message);  console.log(error.suppressed.message);}

Line 8 throws work failed. As the block ends, the cleanup method on line 4 runs and throws cleanup failed. The catch on line 10 receives one error that holds both:

  • Line 11 prints true: the caught error is a SuppressedError.
  • Line 12 prints cleanup failed: the error property holds the cleanup failure.
  • Line 13 prints work failed: the suppressed property holds the earlier work failure.
A cart named error, which is a SuppressedError. Its error property points to the cleanup failed error. Its suppressed property points to the work failed error.SuppressedErrorerror.errorcleanup failed.suppressedwork failed
The caught error keeps both failures. error.error is the cleanup failure, and error.suppressed is the work failure from line 8.

You practiced the same habit in custom errors and error handling strategies: keep every error that explains what went wrong, instead of replacing one with another.

Two failed cleanups keep the first errorPop out in the code editor (opens in a new tab)JavaScript
try {
  using first = { [Symbol.dispose]() { throw Error("first close"); } };
  using second = { [Symbol.dispose]() { throw Error("second close"); } };
  throw Error("work");
} catch (error) {
  console.log(error.error.message);
  console.log(error.suppressed.error.message);
  console.log(error.suppressed.suppressed.message);
}

Line 4 throws work. Then second is cleaned up first, and it throws. first is still cleaned up after that, and it throws too. Each failure wraps the one before it. Lines 6 to 8 print first close, second close, and then work.

To find an earlier error, follow the suppressed property one step at a time. When you report the error, keep all of them.

Practical patterns in apps

BROWSER

A good pattern is a small helper function that returns an object with a cleanup method. The helper sets the resource up and also knows how to clean it up. The code that calls the helper can't pair the wrong cleanup with the wrong setup.

A click listener that removes itselfPop out in the code editor (opens in a new tab)JavaScript
function listen(target, type, handler, options) {  target.addEventListener(type, handler, options);  return {    [Symbol.dispose]() {      target.removeEventListener(type, handler, options);    },  };} const button = document.querySelector("#save") ??  document.body.appendChild(document.createElement("button")); {  using click = listen(button, "click", () => console.log("clicked"));  button.click();} button.click();console.log("listener removed");

Line 1 defines listen(). It adds a listener on line 2. Its cleanup method on line 4 removes the same listener on line 5.

Line 14 adds a click listener with using. The click on line 15 logs clicked, because the listener is still there. When the block ends, the listener is removed. So the click on line 18 logs nothing, and line 19 prints listener removed.

Use this for any listener you need only for a short time, such as while the user drags something across the page.

A preview URL that is revoked at the endJavaScript
function objectUrlFor(blob) {  const url = URL.createObjectURL(blob);  return {    url,    [Symbol.dispose]() {      URL.revokeObjectURL(url);      console.log("revoked object URL");    },  };} {  using preview = objectUrlFor(new Blob(["hello"], { type: "text/plain" }));  console.log(preview.url.startsWith("blob:"));}

URL.createObjectURL() on line 2 makes a blob: URL that points at data in memory, such as a picked photo. The browser keeps that data until you call URL.revokeObjectURL() or the page closes. If you forget, the data stays in memory the whole time.

The cleanup method on line 5 revokes the URL on line 6. Line 13 creates the preview with using, and line 14 prints true. Then the block ends, and the code prints revoked object URL.

In a real app, revoke a preview URL when the preview is replaced or removed. Don't let a short function revoke it while an image on the page still shows it. This example only logs the URL. It doesn't run in the editor sandbox, because a blob URL belongs to the page that created it.

A database change that is undone on failureJavaScript
async function enterTransaction(db) {  const transaction = await db.begin();  return {    transaction,    async [Symbol.asyncDispose]() {      if (transaction.open) await transaction.rollback();    },  };} async function saveInvoice(db, invoice) {  await using scope = await enterTransaction(db);  await scope.transaction.insert(invoice);  await scope.transaction.commit();}

A transaction is a group of database changes that either all happen or none do. Closing one takes time, so this example uses await using on line 12.

  • If insert throws, the transaction is still open, so the cleanup on line 6 undoes it with rollback().
  • If commit() on line 14 succeeds, the transaction is closed, so the cleanup does nothing.

Where using works, and TypeScript

CHECK FIRST

According to MDN's browser data, these support explicit resource management:

  • Chrome and Edge 134 and later
  • Firefox 141 and later
  • Node.js 24 and later
  • Safari Technology Preview, Apple's test version of Safari

That doesn't mean every visitor's browser supports it. Check the runtime, the program that really runs your code, such as a browser or Node. For older runtimes, let TypeScript rewrite the code into older JavaScript.

Feature check: can this browser read using?
Check whether using can be readPop out in the code editor (opens in a new tab)JavaScript
function supportsUsingSyntax() {  try {    new Function("{ using x = null; }");    return true;  } catch {    return false;  }} console.log(supportsUsingSyntax());
Your browserNot checked yet
ResultNot checked yet
Try it yourself

The check runs in your browser, because Node 22 can load this lesson but can't read using.

Line 3 asks the browser to read a tiny using block. If it throws, the browser can't read using. Checking for Symbol.dispose isn't enough, because Node 22 has the symbol but can't read the keyword.

The first table lists what supports the feature. Read each row as: where, which versions, and what to remember.

Support checked for this lesson
WhereWhich versionsWhat to remember
BrowsersChrome and Edge 134+, Firefox 141+, Safari Technology PreviewMDN marks the feature as not yet in every browser, because Safari's normal release doesn't have it yet.
Node.jsNode 24+ understands using. Node 22 doesn't.In modern Node, a FileHandle has [Symbol.asyncDispose]() and a timer has [Symbol.dispose]().
TypeScriptTypeScript 5.2+It accepts using and await using, and can rewrite them into older JavaScript.
Feature checkTry to read the codeRun new Function("{ using x = null; }") inside a try block before you offer using code that runs.

The second table lists where you may write using. Read each row as: the place, whether it works, and an example.

Where you can write using
Where you write itDoes it work?Example
Inside a block or a functionYes{ using handle = open(); } or function f() { using handle = open(); }
At the top level of a moduleYesA module has its own scope, so using works at its top level.
At the top level of a classic scriptNo, it's a SyntaxErrorPut it inside a block, or use <script type="module">.
In a for...of loopYesfor (using row of rows()) cleans up each row before the next turn of the loop.
Classic scripts need a block or moduleHTML
<script>  // SyntaxError in a classic script's top level.  using handle = openHandle();</script> <script type="module">  // Module top level is allowed.  using handle = openHandle();</script> <script>  // Blocks and functions are allowed in classic scripts.  {    using handle = openHandle();  }</script>

A classic script is a plain <script> tag. Line 3 puts using at its top level, outside any block, so it fails. Line 8 is the same line in a module script, <script type="module">, and it works. Line 14 puts the line inside { }, which works in a classic script too.

Node 24+ file and timer handles clean themselves upJavaScript
import { open } from "node:fs/promises";import { setTimeout as delay } from "node:timers/promises"; await using file = await open("report.txt", "r");const text = await file.readFile("utf8"); using timer = setTimeout(() => console.log("too slow"), 5000);await delay(10);console.log(text.length);

In modern Node, a file handle has an [Symbol.asyncDispose]() method, so line 4 uses await using. A timer handle has a [Symbol.dispose]() method, so line 7 uses using.

Watch the version. Node 22 already has Symbol.dispose and Symbol.asyncDispose, but it can't read the using keyword. Node 24 and later can.

TypeScript 5.2+ can rewrite using for older runtimesTypeScript
// TypeScript 5.2+ accepts this syntax.{  using stack = new DisposableStack();  const file = stack.use(openFile());  stack.defer(() => console.log("always runs"));} // For older targets, TypeScript emits helper code with try/finally// and calls Symbol.dispose / Symbol.asyncDispose for you.

TypeScript can rewrite using into older code with try/finally. The rewritten code still needs Symbol.dispose or Symbol.asyncDispose to exist when it runs. If you use DisposableStack, the runtime needs that too, or a polyfill: a small script that adds a missing feature. Check the runtime as well as the TypeScript version.

Common misconceptions and comparisons

  • “The garbage collector will close it.” The garbage collector frees memory. It doesn't promise to close files, release locks, stop timers, remove listeners, or revoke blob URLs when you need it to.
  • “FinalizationRegistry can do the cleanup.” A FinalizationRegistry calls a function some time after an object is removed from memory. It may run late, or never. Use it only as a backup. See WeakRef & FinalizationRegistry.
  • “using catches errors.” It doesn't. It cleans up as the code leaves the block. Then the return or the error carries on as normal.
  • “The first resource closes first.” It closes last. The resource added last is cleaned up first.
  • “If Symbol.dispose exists, I can write using.” Not always. Node 22 has the symbol, but it can't read the using keyword.
  • “No other language does this.” C# has using and Python has with for the same job. JavaScript's version uses built-in symbols, with one method for normal cleanup and one for cleanup that takes time.
A finalizer may run late, or neverJavaScript
const registry = new FinalizationRegistry((id) => {  console.log("maybe later", id);}); function cacheImage(image) {  registry.register(image, image.id);  return new WeakRef(image);} // You cannot predict when, or whether, the callback runs.

Line 1 creates the registry. Line 6 asks it to call the function after image is removed from memory. As line 10 says, you can't know when that happens, or whether it happens at all.

The table compares three ways to clean up. Read each column as one tool, and each row as one question.

Similar ideas, different promises
Questionusing and stacksFinalizationRegistryC# using and Python with
When does cleanup run?Right when the code leaves the block.Some time after the object leaves memory, or never.Right when the code leaves the block.
What gets called?[Symbol.dispose]() or [Symbol.asyncDispose]().The function you gave the registry.Dispose() in C#. __exit__ or __aexit__ in Python.
What is it for?Files, locks, timers, listeners, blob URLs, and transactions.Backup cleanup and debugging help.The same kind of resources, in those languages.

The method names come from Symbols and well-known symbols. JavaScript uses built-in symbols to let your objects take part in language features.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the cleanup order

Read the code. Type the exact text that the last line prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const log = [];
function resource(name) {
  return { [Symbol.dispose]() { log.push(`dispose ${name}`); } };
}
{
  using one = resource("one");
  using two = resource("two");
  log.push("body");
}
console.log(log.join(" -> "));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the cleanup method

    What method name makes this object work with using?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const handle = {
      // Write the well-known symbol method here.
    };

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName the cleanup method that takes time

      Which symbol goes inside the square brackets?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const connection = {
        async [/* which symbol goes here? */]() {
          await closeConnection();
        },
      };

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRead the stack's cleanup order

        When the stack is cleaned up, which message prints first?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const stack = new DisposableStack();
        stack.defer(() => console.log("first registered"));
        stack.defer(() => console.log("second registered"));
        stack.dispose();

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeClean up an image preview

          A preview makes a blob URL for a file the user picked. What call belongs in the cleanup method?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function showPreview(file, preview) {
            const url = URL.createObjectURL(file);
            preview.src = url;
            // What cleanup call belongs in the disposer?
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What problem does explicit resource management solve?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this using block print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const log = [];
              function resource(name) {
                return { [Symbol.dispose]() { log.push(`dispose ${name}`); } };
              }
              {
                using one = resource("one");
                using two = resource("two");
                log.push("body");
              }
              console.log(log.join(" -> "));

              Choose an answer to see the explanation.

            3. Question 3 of 7What happens when a using value is null or undefined?

              Choose an answer to see the explanation.

            4. Question 4 of 7Which method does await using try first when the block ends?

              Choose an answer to see the explanation.

            5. Question 5 of 7When this stack is cleaned up, what prints first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const stack = new DisposableStack();
              stack.defer(() => console.log("first registered"));
              stack.defer(() => console.log("second registered"));
              stack.dispose();

              Choose an answer to see the explanation.

            6. Question 6 of 7When does JavaScript create a SuppressedError?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which statement about support is true today?

              Choose an answer to see the explanation.

            Key takeaways

            • using calls [Symbol.dispose]() when the code leaves the block.
            • await using waits for [Symbol.asyncDispose]() when the code leaves the block.
            • The resource created last is cleaned up first, even after an error or an early return.
            • DisposableStack and AsyncDisposableStack collect cleanup from many places in one spot.
            • SuppressedError keeps both the cleanup error and the error that came before it.
            • FinalizationRegistry may run late or never, so use it only as a backup.

            Remember the one-liner.
            Give each resource to the smallest block that can clean it up, and let the block do the cleanup for you.

            This is the last lesson in Binary data & memory, and in the Advanced language features stage. Next, Professional JavaScript starts with npm and package.json.

            CompleteFrontend Clear concepts. Working examples.