Node.js/TypeScriptの`await using`でリソース管理を安全に
こんにちは!製造ビジネステクノロジー部の石井です。
ファイルを開いて処理して閉じる、よくあるファイル操作パターンですが、毎回クローズ漏れを気にしながら実装するのって煩わしくないですか?
「Javaなら try-with-resources で勝手に閉じてくれるのに...」とずっと思ってたんですが、調べてみたらNode.js/TypeScriptにも同じような仕組みがありました。実際に試してみたので紹介します。
よくあるリソースの閉じ忘れパターン
こんなコード、見たことありませんか?ファイルを開いて、データを読み取って、閉じる関数です。
const processFile = async (filePath: string) => {
const handle = await open(filePath);
const data = await handle.read();
await doSomething(data);
handle.close();
};
一見問題なさそうに見えますが、 handle.read() や doSomething が例外を投げたら、handle.close() は実行されません。
ファイルが開きっぱなしになって、最悪「ファイルが開けません」エラーやメモリリークの原因になります。
修正版はこんな感じ。
const processFile = async (filePath: string) => {
let handle: FileHandle | null = null;
try {
handle = await open(filePath);
const data = await handle.read();
await doSomething(data);
} finally {
if (handle) {
handle.close();
}
}
};
オプショナルチェーンを使えばもう少し短くなります。
const processFile = async (filePath: string) => {
let handle: FileHandle | null = null;
try {
handle = await open(filePath);
const data = await handle.read();
await doSomething(data);
} finally {
handle?.close();
}
};
let で null 初期化して、try-finally で囲んで、nullチェックして...リソースを1つ閉じるだけなのに、ボイラープレートが多すぎませんか?
Javaなら1行で済む
同じことをJavaで書くとこうなります。
try (ZipFile zipFile = new ZipFile(zipPath)) {
// zipFileを使った処理
}
// スコープを抜けると自動的にzipFile.close()が呼ばれる
Javaの try-with-resources 構文ですね。AutoCloseable インターフェースを実装したオブジェクトなら、成功でもエラーでも try ブロックを抜けるときに自動で close() が呼ばれます。
Node.jsにはこの仕組みがなかったので、try-finally を毎回手書きするしかないと思ってました。
(ちなみにRustなら Drop トレイトによってスコープを抜けると自動で解放されるので、特別な構文すら不要です。)
await using の登場
実はTypeScript 5.2(2023年8月リリース)では、await using という同じような仕組みが使えるようになっていました。全然知らなかった...。
const processFile = async (filePath: string) => {
await using handle = await withAutoClose(filePath);
const data = await handle.read();
await doSomething(data);
// スコープを抜けると自動的にhandle[Symbol.asyncDispose]()が呼ばれる
};
try-finally もnullチェックも消えました。
Javaの try-with-resources とほぼ同じ感覚で書けますね。
仕組みを理解する
Symbol.asyncDispose とは
await using で宣言されたオブジェクトは、スコープを抜けるときに Symbol.asyncDispose メソッドが自動で呼ばれます。Javaでいう AutoCloseable の close() に相当するやつです。
| Java | Node.js / TypeScript |
|---|---|
AutoCloseable インターフェース |
Symbol.asyncDispose メソッド |
close() |
[Symbol.asyncDispose]() |
try-with-resources |
await using |
Javaエンジニアならこの対応表を見れば「あ、そういうことね」ってなると思います。
ライブラリが対応していない場合
現時点では Symbol.asyncDispose を実装しているライブラリはまだ少ないです。どうしても使いたい場合は自分でラッパーを書けば使えます。
const withAutoClose = async (path: string) => {
const handle = await open(path);
return Object.assign(handle, {
[Symbol.asyncDispose]: async () => {
handle.close();
},
});
};
// 使う側
await using handle = await withAutoClose(filePath);
close() メソッドを持つオブジェクトなら何でも対応できるように汎用化することもできます。
const withAutoClose = <T extends { close: () => void | Promise<void> }>(
resource: T,
) => {
return Object.assign(resource, {
[Symbol.asyncDispose]: async () => {
await resource.close();
},
});
};
// ファイルでもDB接続でも使える
await using handle = withAutoClose(await open(filePath));
await using connection = withAutoClose(await getDBConnection());
一応、ラッパー関数を作らずにその場で書く方法もあります。
const handle = await open(filePath);
await using _ = {
[Symbol.asyncDispose]: async () => handle.close(),
};
ただし、handle と _ が別々の変数になっていて、パッと見では何のための定義なのかわかりにくいです。保守性を考えると、素直に withAutoClose のようなラッパーを使う方が良いと思います。
複数リソースの解放順序
await using を複数宣言すると、宣言の逆順でdisposeされます。
await using db = await connectDB(); // 2番目に閉じられる
await using connection = await db.pool(); // 1番目に閉じられる
これもJavaの try-with-resources と同じ挙動ですね。依存関係があるリソース(DBプール → コネクション)でも安全に閉じられます。
実際に試してみた
構文はわかったけど、ほんとにちゃんとdisposeされるの?ということで、簡単なモジュールを作って検証してみました(Node.js v24で実行)。
検証用のリソースモジュール
まず、開閉がログで見えるリソースを作ります。
// disposable-resource.ts
/** await usingで自動解放されるリソースを作成する */
const createResource = (name: string): Resource => {
console.log(`[${name}] opened`);
return {
name,
doWork: () => {
console.log(`[${name}] working...`);
},
[Symbol.asyncDispose]: async () => {
console.log(`[${name}] closed`);
},
};
};
interface Resource extends AsyncDisposable {
/** リソース名 */
name: string;
/** 何か作業をする */
doWork: () => void;
}
AsyncDisposable はTypeScriptに組み込まれているインターフェースで、Symbol.asyncDispose メソッドを持つことを表します。Javaの AutoCloseable に対応するやつですね。
テスト1: 正常系 ― スコープを抜けたらdisposeされる?
const normalCase = async () => {
console.log('--- 正常系 ---');
await using resource = createResource('DB接続');
resource.doWork();
console.log('スコープ終了');
};
await normalCase();

ちゃんと「スコープ終了」の後に closed が出てますね。try-finally を書かなくても自動で閉じてくれてます。
テスト2: エラー発生時 ― 例外が飛んでもdisposeされる?
const errorCase = async () => {
console.log('--- エラー系 ---');
try {
await using resource = createResource('ファイル');
resource.doWork();
throw new Error('処理中にエラー発生!');
} catch (e) {
console.log(`caught: ${(e as Error).message}`);
}
};
await errorCase();

例外が投げられても closed が呼ばれてます。しかも closed → caught の順番になっているのがポイントで、disposeが先に実行されてからcatchブロックに入っています。
テスト3: 複数リソース ― 逆順で閉じられる?
const multipleResources = async () => {
console.log('--- 複数リソース ---');
await using first = createResource('1番目');
await using second = createResource('2番目');
await using third = createResource('3番目');
console.log('全部開いた');
};
await multipleResources();

宣言の逆順(3 → 2 → 1)で閉じられてます。Javaの try-with-resources と同じ挙動ですね。
依存関係があるリソースでも安全に解放できることがわかります。
テスト4: dispose自体がエラーを投げたらどうなる?
const disposeErrorCase = async () => {
console.log('--- dispose中のエラー ---');
try {
await using resource = {
[Symbol.asyncDispose]: async () => {
throw new Error('closeに失敗!');
},
};
console.log('処理完了');
} catch (e) {
console.log(`caught: ${(e as Error).message}`);
}
};
await disposeErrorCase();

dispose中のエラーもちゃんとcatchできます。
ちなみにJavaの try-with-resources だと、本体の処理と close() の両方が例外を投げた場合、close() の例外は「suppressed exception」として本体の例外に紐づけられます。明示的に getSuppressed() を呼ばないと気づけないので、知らないとハマりがちです。
Node.jsの await using ではそういった仕組みはなく、disposeのエラーもそのまま普通にcatchブロックで受け取れます。この辺はNode.jsの方がシンプルですね。
TC39ステージと対応状況
そもそもTC39ステージって何?
await using の仕様がどういう状態なのか理解するには、TC39のステージ制度を知っておく必要があります。
TC39(Technical Committee 39)はJavaScriptの言語仕様(ECMAScript)を策定している委員会です。新しい構文を追加するには、以下のステージを通過しないといけません(TC39 Process)。
| Stage | 意味 |
|---|---|
| 0 | アイデア段階 |
| 1 | 問題と解決策が明確になった提案 |
| 2 | 仕様の草案ができた段階 |
| 2.7 | テストが書かれ、実装準備OK |
| 3 | 仕様がほぼ確定。ランタイムが実装を始める段階 |
| 4 | 正式にECMAScript仕様に組み込まれた |
Explicit Resource Management(using / await using)は現在 Stage 3 です(TC39 Proposals、提案リポジトリ)。仕様は固まっているけど、まだ正式な標準(Stage 4)にはなっていません。
ランタイム・ブラウザ対応状況(2026年3月時点、Can I use)
| 環境 | 対応状況 |
|---|---|
| Node.js | v24〜 対応 |
| Chrome | v134〜 対応 |
| Edge | v134〜 対応 |
| Firefox | v141〜 対応 |
| Safari | 未対応 |
| TypeScript | 5.2〜 対応 |
| Deno | 対応(公式ドキュメント) |
Node.jsについては、Dockerで各バージョンのコンテナを立ち上げて、TypeScriptのトランスパイルを通さずに純粋なJavaScriptとして await using が動くか検証してみました。
docker run --rm node:22-slim node --input-type=module -e '
async function test() {
await using r = { [Symbol.asyncDispose]: async () => console.log("closed") };
console.log("OK");
}
test();
'
| Node.js | await using |
|---|---|
| v24 | OK |
| v23 | SyntaxError |
| v22 | SyntaxError |
| v20 | SyntaxError |
| v18 | SyntaxError |
v23以下では以下のようなエラーが出ます。
await using r = { [Symbol.asyncDispose]: async () => console.log("closed") };
^
SyntaxError: Unexpected identifier 'r'
V8エンジンが await using を構文として認識できず、using の後の r を不正な識別子として拒否しています。
v24からネイティブ対応です。なお Symbol.asyncDispose(シンボルだけ)はv18.18.0から存在していますが、await using 構文自体はV8エンジン側の対応が必要で、v24に搭載されたV8 v13.6で初めてサポートされました。
TypeScriptのトランスパイル(tsx等)を通せばv22以下でも動きますが、ネイティブで使いたい場合はv24以上が必要です。
まとめ
リソースの閉じ忘れパターンをきっかけに、Node.js/TypeScriptの await using を調べて実際に試してみました。
- Javaの
try-with-resourcesと同じ感覚で、スコープを抜けたら自動クリーンアップできる - 正常系はもちろん、エラー発生時やdispose自体のエラーもきちんと処理される
- ライブラリが未対応でも
withAutoCloseのようなラッパーを書けば使える - ただしNode.jsでネイティブに動くのはv24から。v23以下では
SyntaxErrorになる(Dockerで検証済み) - TC39ではまだStage 3で、Safariも未対応なのでフロントエンドでの利用は条件次第
サーバーサイドの try-finally に疲れた方、Node.js v24環境ならぜひ試してみてください。
おまけ
2026年3月26日(木)に、クラスメソッド名古屋オフィス(伏見駅徒歩5分)で 「なごやクラメソゆる勉強会」 を開催します!
第1回のテーマは「最近やってみたこと」のLT大会です。
途中退出OK、LTを聞くだけでもOK。LT後には交流タイムもあるので、LTで気になったことを登壇者に聞いてみたり、「最近これ気になってるんですけど...」みたいな雑談をしたり、なんでも気軽に話せます。
今後も定期的にイベントを開催していく予定で、こちらで随時更新していく予定です!ご興味ある方はぜひ!






