JavaScript 的 using 可避免為清理資源巢狀使用 try/finally
Stop Nesting try/finally for Cleanup. JavaScript Has using
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
Not at the top level of a classic script. MDN says
usingis not allowed at the top level of a script, and Node'svm.ScriptthrowsSyntaxError: Unexpected identifier 'a'forusing a = null;. Modules, functions and blocks are fine. A CommonJS file works too, because Node wraps it in a function.The value must be disposable, or nothing.
using x = { name: "no dispose" }throwsTypeError: Symbol(Symbol.dispose) is not a function.nullandundefinedare allowed and simply skipped, which is handy for optional resources.Cleanup errors do not swallow the real error. If the block throws and a disposer throws too, you get a
SuppressedError. Itserroris the cleanup failure and itssuppressedis the original one, so check both before blaming the wrong line.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.
-
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 callmove()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
- MDN: using
- MDN: DisposableStack
- MDN: Firefox 141 release notes for developers
- MDN browser-compat-data: DisposableStack
- Web Platform Status: Explicit resource management
- TC39: Finished proposals
- TC39: Explicit Resource Management proposal
This article was written with the help of AI and checked against the sources above.
來源:DEV Community · dev.to