Node.js/TypeScriptの`await using`でリソース管理を安全に

Node.js/TypeScriptの`await using`でリソース管理を安全に

Node.jsでもJavaのtry-with-resourcesのように、スコープを抜けたら自動でリソースを閉じる仕組みがあることを知りました。 await using構文を実際に検証してみます。
2026.03.18

こんにちは!製造ビジネステクノロジー部の石井です。

ファイルを開いて処理して閉じる、よくあるファイル操作パターンですが、毎回クローズ漏れを気にしながら実装するのって煩わしくないですか?
「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();
  }
};

letnull 初期化して、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でいう AutoCloseableclose() に相当するやつです。

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();

CleanShot 2026-03-18 at 00.22.19@2x.jpg

ちゃんと「スコープ終了」の後に 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();

CleanShot 2026-03-18 at 00.24.38@2x.jpg

例外が投げられても closed が呼ばれてます。しかも closedcaught の順番になっているのがポイントで、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();

CleanShot 2026-03-18 at 00.25.05@2x.jpg

宣言の逆順(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();

CleanShot 2026-03-18 at 00.28.44@2x.jpg

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で気になったことを登壇者に聞いてみたり、「最近これ気になってるんですけど...」みたいな雑談をしたり、なんでも気軽に話せます。

なごやクラメソゆる勉強会 - connpass

会場(ラウンドテラス伏見8階) - Google マップ

今後も定期的にイベントを開催していく予定で、こちらで随時更新していく予定です!ご興味ある方はぜひ!

この記事をシェアする

FacebookHatena blogX

関連記事