跳到正文
DEV Community· JKC·· 2 小時前AI 評分64

JavaScript 的 using 可避免為清理資源巢狀使用 try/finally

Stop Nesting try/finally for Cleanup. JavaScript Has using

AI 導讀

JavaScript 的 using 和 await using 會在作用域結束時自動呼叫資源的 [Symbol.dispose]() 或 [Symbol.asyncDispose](),即使程式拋出錯誤也會執行清理。

正文

TL;DR: using file = ... calls file[Symbol.dispose]() the moment the block ends, whether it returned or threw. It ships in Chrome 134+, Edge 134+, Firefox 141+, Node 24+, Deno and Bun, but Safari has it only in Technology Preview, so it is not Baseline yet.

The pyramid nobody wants to touch

Every codebase has a function shaped like this:

const file = await open(path, "w");
try {
  const timer = setInterval(report, 1000);
  try {
    await writeEverything(file);
  } finally {
    clearInterval(timer);
  }
} finally {
  await file.close();
}

Two resources, two finally blocks, four levels of indentation. Add a third resource and the function starts drifting off the right side of the screen. Forget one finally and you leak a file handle that only shows up in production at 3 a.m.

The cleanup is always the same: undo things in the reverse order you did them. That is exactly the kind of bookkeeping a language should do for you.

What it is

Explicit resource management adds two declarations and a few helpers:

Piece What it does
using x = value Calls x[Symbol.dispose]() when the scope exits
await using x = value Awaits x[Symbol.asyncDispose]() when the scope exits
DisposableStack Collects cleanups by hand with use(), defer(), adopt() and move()
AsyncDisposableStack The same, for async cleanups
SuppressedError Carries a cleanup error together with the error it interrupted

It is a TC39 Stage 4 proposal, listed in TC39's finished proposals with an expected publication year of 2027.

A minimal example

Any object with a [Symbol.dispose]() method can be using-ed:

function resource(name) {
  console.log(`open ${name}`);
  return { [Symbol.dispose]() { console.log(`close ${name}`); } };
}

{
  using db = resource("db");
  using cache = resource("cache");
  console.log("working");
}
console.log("after the block");

Output from Node 24.11:

open db
open cache
working
close cache
close db
after the block

Last in, first out. The cache is closed before the database it might depend on, and you did not write a single finally.

A realistic example: writing a file with a progress timer

Node's FileHandle already has [Symbol.asyncDispose], so await using works on it out of the box. A timer is not disposable, but DisposableStack.defer() turns any cleanup function into one.

import { open, readFile } from "node:fs/promises";

let lastFile; // only here so we can peek after the function returns

async function exportCsv(path, rows) {
  await using file = await open(path, "w");
  lastFile = file;

  using stack = new DisposableStack();
  const timer = setInterval(() => console.log("still writing..."), 1000);
  stack.defer(() => {
    clearInterval(timer);
    console.log("timer cleared");
  });

  for (const row of rows) {
    if (!Array.isArray(row)) throw new Error(`bad row: ${row}`);
    await file.write(row.join(",") + "\n");
  }
  return rows.length;
}

console.log("wrote", await exportCsv("ok.csv", [["id", "name"], [1, "Ada"]]), "rows");
console.log("fd after success:", lastFile.fd);
console.log(JSON.stringify(await readFile("ok.csv", "utf8")));

try {
  await exportCsv("bad.csv", [["id"], "oops"]);
} catch (e) {
  console.log("caught:", e.message);
}
console.log("fd after failure:", lastFile.fd);

Output from Node 24.11:

timer cleared
wrote 2 rows
fd after success: -1
"id,name\n1,Ada\n"
timer cleared
caught: bad row: oops
fd after failure: -1

An fd of -1 means the handle is closed. Happy path or thrown error, the timer is cleared and the file is closed, in that order. The pyramid from the top of this post is now flat.

Can I use it?

On the server, yes: Node has it from 24.0, Deno from 2.2.10 and Bun from 1.3.0, according to MDN's compatibility data.

In the browser, mostly. Chrome and Edge shipped it in 134, and Firefox in 141. Safari lists it only as "preview", which means Safari Technology Preview, and MDN marks the feature as Limited availability. If you ship to Safari users, keep the try/finally version or let your build tool transpile using for now.

Gotchas

  1. Not at the top level of a classic script. MDN says using is not allowed at the top level of a script, and Node's vm.Script throws SyntaxError: Unexpected identifier 'a' for using a = null;. Modules, functions and blocks are fine. A CommonJS file works too, because Node wraps it in a function.

  2. The value must be disposable, or nothing. using x = { name: "no dispose" } throws TypeError: Symbol(Symbol.dispose) is not a function. null and undefined are allowed and simply skipped, which is handy for optional resources.

  3. Cleanup errors do not swallow the real error. If the block throws and a disposer throws too, you get a SuppressedError. Its error is the cleanup failure and its suppressed is the original one, so check both before blaming the wrong line.

  4. Closures can outlive the resource. This returns a function that reads an already closed resource:

function getReader() {
  using r = { open: true, [Symbol.dispose]() { this.open = false; } };
  return () => r.open;
}
console.log("still open?", getReader()()); // still open? false

using ties the lifetime to the block, not to whoever holds a reference.

  1. Constructors want move(). When an object opens two resources and the second one fails, the first should be closed. Build them on a stack and call move() only after everything succeeded:
class Connection {
  #stack;
  constructor() {
    using stack = new DisposableStack();
    this.db = stack.use(openDb());
    this.cache = stack.use(openCache()); // if this throws, db is closed
    this.#stack = stack.move();          // success: we own them now
  }
  [Symbol.dispose]() { this.#stack.dispose(); }
}

In Node 24.11, a failing openCache() printed open db, close db, then the error. On success, nothing closes until the caller's own using conn = new Connection() goes out of scope.

Wrap-up

Cleanup code was always the part we copy, paste and forget. Find your deepest try/finally pyramid, give its resources a [Symbol.dispose], and let the block end do the work.

Sources

This article was written with the help of AI and checked against the sources above.

來源:DEV Community · dev.to