📋 目錄
這篇會帶你看懂
using/await using在 2026 年能做什麼、不能做什麼,以及如何用可驗證方式導入到前端與 Node 專案。
前言
try...finally 是 JavaScript 團隊熟悉到不能再熟悉的 cleanup 模式。問題不在「你不知道要 cleanup」,而在真實專案裡它很容易變成散落在多層流程中的 maintenance burden:早退、拋錯、巢狀資源、測試 teardown 都會讓釋放邏輯變得脆弱。
TC39 的 Explicit Resource Management(ERM)就是針對這個痛點提出的語言級方案,用 using / await using 明確綁定資源生命週期,讓 cleanup 在離開 scope 時自動執行。
但這裡要先校正一件最重要的事:截至 2026-04-01,ERM 在 proposal repo 仍標示 Stage 3,不是 Stage 4,也不等於所有 runtime 都已全面原生可用。這篇的重點就是把「語法支援」和「runtime 可落地」拆清楚。
這個提案到底在解什麼問題
ERM 的核心不是取代 try...finally,而是把資源釋放從「慣例」變成「語法級生命週期」。
先看傳統寫法:
import { open } from "node:fs/promises";
export async function readConfig(path: string) {
const file = await open(path, "r");
try {
return await file.readFile({ encoding: "utf8" });
} finally {
await file.close();
}
}
邏輯本身沒問題,但當你同時管理 2~3 個資源,或中間再包一層 helper,很容易出現「哪個 finally 負責哪個資源」的閱讀成本。
ERM 做的事很單純:
- 宣告時註記資源生命週期(
using/await using)。 - 離開目前 scope 時自動呼叫對應釋放方法。
- 多個資源依宣告反向順序釋放(LIFO)。
這是一種語意明確化,不是魔法。它不會替既有 close() API 自動套上 protocol,也不會幫你修正不正確的資源設計。
using 與 await using 真正做了什麼
using: 同步釋放(Symbol.dispose)
class ReaderLockResource implements Disposable {
constructor(private readonly reader: ReadableStreamDefaultReader<Uint8Array>) {}
[Symbol.dispose]() {
// releaseLock() 是同步 API,適合用 using
this.reader.releaseLock();
}
}
function consumeOnce(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
using lock = new ReaderLockResource(reader);
// 中途 throw 也會在離開 scope 時 release lock
return reader.read();
}
using 需要的是 [Symbol.dispose](),不是 .dispose() 字串方法。
await using: 非同步釋放(Symbol.asyncDispose)
import { open } from "node:fs/promises";
export async function firstLine(path: string) {
// 第一個 await:等待 open() 拿到 FileHandle
// await using:離開 scope 時 await file[Symbol.asyncDispose]()
await using file = await open(path, "r");
const text = await file.readFile({ encoding: "utf8" });
return text.split("\n")[0] ?? "";
}
await using 跟一般 await 一樣有語法上下文限制:只能在 async function 內,或 module top-level。
如果資源不符合 protocol,會怎樣?
function demoTypeError() {
// 這個物件沒有 [Symbol.dispose]
using bad = { close() {} } as { close(): void };
// 會在宣告點丟 TypeError(runtime)
return bad;
}
using / await using 是 opt-in。只有實作對應 symbol 的物件,才是可管理資源。
TypeScript 與 runtime 支援:你要同時過兩關
很多團隊在這裡踩坑,是因為把「TypeScript 可編譯」誤認為「runtime 已可跑」。實際上是兩個獨立層次。
關卡 1:TypeScript 編譯器層
TypeScript 5.2+ 提供語法與型別支援,但你需要正確設定 target 與 lib。
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"lib": ["es2022", "esnext.disposable", "dom"],
"strict": true
}
}
如果你的 lib 沒含 esnext.disposable(或整包 esnext),Disposable / AsyncDisposable 型別與相關 symbol 會出現型別缺口。
關卡 2:runtime 層
TypeScript 5.2 release notes 也明確提醒:這個特性在很多 runtime 上不一定完整原生,必要時要補 polyfill(至少 Symbol.dispose、Symbol.asyncDispose)。
// 最低限度 symbol polyfill(需在程式進入點最先執行)
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
若你還要用 DisposableStack / AsyncDisposableStack / SuppressedError,就不是只補 symbol 這麼簡單,通常要用完整 polyfill 套件或 runtime 原生支援。
Node.js 情境要特別注意版本邊界
以 fs/promises 的 FileHandle[Symbol.asyncDispose]() 為例,Node 文件可查到:
v20.4.0/v18.18.0已加入- 到
v24.2.0標示為不再 experimental
這代表你在 Node 專案裡用 await using file = await open(...) 時,應該明確寫進團隊的最低 Node 版本政策,而不是只在本機「剛好能跑」。
三個可驗證範例(前端工程情境)
範例 1:ReadableStream reader lock(同步 cleanup)
class StreamReaderResource implements Disposable {
constructor(private readonly reader: ReadableStreamDefaultReader<Uint8Array>) {}
[Symbol.dispose]() {
this.reader.releaseLock();
}
}
export async function readChunk(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
using resource = new StreamReaderResource(reader);
const { value, done } = await reader.read();
if (done || !value) return new Uint8Array();
return value;
}
重點是 releaseLock() 本身是同步行為,所以用 using(不是 await using)較合理。
範例 2:Node 臨時檔案流程(非同步 cleanup)
import { mkdtemp, open, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
class TempDirResource implements AsyncDisposable {
constructor(private readonly dirPath: string) {}
async [Symbol.asyncDispose]() {
await rm(this.dirPath, { recursive: true, force: true });
}
}
export async function writeTempJson(payload: unknown) {
const dir = await mkdtemp(join(tmpdir(), "erm-demo-"));
await using tempDir = new TempDirResource(dir);
const filePath = join(dir, "payload.json");
await using file = await open(filePath, "w");
await file.writeFile(JSON.stringify(payload), "utf8");
return filePath;
}
這個例子同時示範兩個 await using,離開 scope 時會依 LIFO 順序釋放。
範例 3:測試中的可預期 cleanup(自訂 resource)
class MockServerResource implements AsyncDisposable {
private closed = false;
constructor(private readonly server: { close: () => Promise<void> }) {}
async [Symbol.asyncDispose]() {
if (this.closed) return;
this.closed = true;
await this.server.close();
}
}
export async function runCase(createServer: () => Promise<{ close: () => Promise<void> }>) {
await using mock = new MockServerResource(await createServer());
// 執行測試流程
// 即使 throw,server 仍會被關閉
}
這類模式在 integration test 很有價值,因為 cleanup 一致性比語法新奇更重要。
什麼情況值得用,什麼情況先不要用
值得用
- 你有明確「取得-釋放」生命週期的資源(file handle、reader lock、測試 server、DB connection wrapper)。
- 你已經在 TypeScript 層建立可驗證 contract(
Disposable/AsyncDisposable)。 - 你能控管 runtime 版本或 polyfill 初始化順序。
先不要急著全專案改
- 既有 API 只有
.close()/.dispose(),但沒有 symbol protocol,短期內也不打算包裝。 - 執行環境過多(多 Node 版本、邊緣 runtime、舊瀏覽器)且目前沒統一 compat policy。
- 團隊連
try...finally基本 discipline 都還沒建立,直接改語法只會把問題搬家。
務實做法通常是「先在高風險資源點導入」,例如測試資源與 file IO,再逐步推到其他模組。
常見問題 / 注意事項
1. using 是不是比 try...finally 更安全?
在「cleanup 一致執行」這件事上,using 通常更不容易漏;但它不是萬能。資源本身若沒正確實作 [Symbol.dispose] / [Symbol.asyncDispose],一樣會失敗。
2. await using 可以在一般函式用嗎?
不行。它需要可使用 await 的語法環境,也就是 async function 或 module top-level。
3. 我已經有 .close(),一定要改整個 API 嗎?
不一定。你可以先加一層小 wrapper,把 .close() 映射到 [Symbol.asyncDispose](),讓呼叫端先受益,再評估是否把 symbol protocol 納入正式 API。
4. DisposableStack 一定要用嗎?
不一定。單一資源用 using 就夠。DisposableStack / AsyncDisposableStack 比較適合動態收集多個資源且需要集中釋放的場景。
總結
Explicit Resource Management 在 2026-04-01 的正確定位是:提案仍在 Stage 3,但已足夠讓團隊在可控範圍做實務試點。你真正要管理的不是語法,而是三件事:
- 型別層是否明確(
esnext.disposable、Disposablecontract) - runtime 層是否可保證(版本或 polyfill)
- 團隊層是否有導入邊界(先小範圍、高價值場景)
把這三件事講清楚,using 才會是降低錯誤率的工程工具,而不是新語法展示。
參考資料
- TC39 proposal repo(Explicit Resource Management,Stage 3)
- TypeScript 5.2 release notes(
using/await using、esnext.disposable、polyfill 注意事項) - MDN JavaScript resource management /
using/await using - Node.js
fs文件(FileHandle[Symbol.asyncDispose]()版本歷程)