世界を動かす技術を、日本語で。

JavaScriptの新しいスーパーパワー:明示的リソース管理

2025年5月17日原文(v8.dev)

概要

  • Explicit Resource Management 提案は、リソース管理を決定的かつ明示的に行う新機能をJavaScriptに導入
  • using/await using 宣言により、リソーススコープ終了時に自動でクリーンアップ処理を実行
  • DisposableStack/AsyncDisposableStack で複数リソースの効率的な一括管理が可能
  • SuppressedError でリソース解放時のエラーと本体エラーの両方を保持
  • Chromium 134/V8 v13.8/Chrome 134/Firefox 134でサポート、Safari・Node.js未対応

明示的リソース管理(Explicit Resource Management)提案の解説

提案の概要と追加要素

  • Explicit Resource Management は、ファイルハンドルやネットワーク接続などのリソースのライフサイクルを明示的かつ決定論的に管理することを目的とする提案
  • using および await using 宣言を追加し、スコープ終了時に自動で dispose メソッドを呼び出すことを保証
  • Symbol.dispose および Symbol.asyncDispose シンボルにより、クリーンアップ処理を明確化
  • DisposableStack (同期用)・ AsyncDisposableStack (非同期用)というグローバルオブジェクトで、複数のディスポーザブルリソースを一括管理
  • SuppressedError 型で、リソース解放時のエラーと本体処理中のエラーの両方を保持・管理

using/await using 宣言の特徴

  • using宣言 は同期リソース用で、スコープ終了時に Symbol.dispose を自動呼び出し
  • await using宣言 は非同期リソース用で、Symbol.asyncDispose をawait付きで自動呼び出し
  • これにより、同期・非同期リソース両方のリーク防止とコード品質向上を実現
  • using/await using キーワードはブロック内(関数・for文など)でのみ利用可能、トップレベルでは不可
  • 例:ReadableStreamDefaultReaderreader.releaseLock() を自動で呼び出すことで、ストリームのロック解除忘れによるバグを防止

典型的な利用例と従来の課題

  • 従来は try...finally でリソース解放処理(例:releaseLock())を保証する必要があった
    • 例:
      try {
        // 読み取り処理
      } finally {
        reader.releaseLock();
      }
      
  • 今後は、using でラップしたディスポーザブルオブジェクトを使うことで、手動の解放忘れを防止
    • 例:
      using readerResource = {
        reader: response.body.getReader(),
        [Symbol.dispose]() { this.reader.releaseLock(); }
      };
      // 読み取り処理
      // スコープ終了時に自動でreleaseLock()実行
      

DisposableStack / AsyncDisposableStack の活用

  • DisposableStack および AsyncDisposableStack は、複数のリソースを一括でスタック管理できる新しいグローバルオブジェクト

  • スタックにリソースやクリーンアップ処理を追加し、スコープ終了時や明示的なdispose呼び出しで逆順に解放

  • 依存関係がある複数リソースの管理・解放順序を自動化

    • 主なメソッドと用途:

      • use(value):ディスポーザブルリソースをスタックに追加
      • adopt(value, onDispose):非ディスポーザブルリソースと解放コールバックをスタックに追加
      • defer(onDispose):コールバックのみをスタックに追加(リソース非依存のクリーンアップ用)
      • move():スタック内の全リソースを新しいスタックへ移動、所有権移譲
      • dispose()/disposeAsync():スタック内リソースを逆順で解放
    • 例:

      using stack = new DisposableStack();
      stack.use(readerResource); // リソース追加
      stack.dispose(); // 逆順で解放
      

SuppressedError の役割

  • SuppressedError は、リソース解放時に発生したエラーと、もともと発生していたエラーの両方を保持するエラー型
  • 例:本体処理のエラーと解放処理のエラーが同時に発生した場合、どちらも追跡・デバッグ可能

サポート状況

  • Chromium 134 および V8 v13.8 以降で実装済み
    • Chrome :バージョン134以降でサポート
    • Firefox :バージョン134以降でサポート
    • Safari :未サポート
    • Node.js :未サポート
    • Babel :サポートあり

まとめ

  • Explicit Resource Management により、JavaScriptでのリソース管理がより堅牢・効率的・保守性の高いものになることを実現
  • using/await usingDisposableStack/AsyncDisposableStackSuppressedError などの導入で、手動管理の負担やバグリスクを大幅に削減
  • 今後のWeb API(例:Streams)へのシンボル統合も期待されるため、最新動向の確認と活用推進が重要

Hackerたちの意見

誰か、なんで(匿名)クラスのデストラクタを使わなかったのか説明してくれない?特に、シンボル以外の特別なオブジェクトキーを使うとかさ。非同期用の別のシンボルもあるし、これって漏れやすい抽象化になっちゃうんじゃない?

このアプローチは、クラスのインスタンスじゃないものにも使えるからね。

他の言語のデストラクタは、通常オブジェクトがガベージコレクトされるときに使われるんだ。でも、それにはいろんな問題が伴うから、最近はこのパターンを避けることが多いんだよね。一方で、disposeメソッドは変数がスコープを出るときに呼ばれるから、もっと予測可能なんだ。例えば、メソッドが返る前にファイルが閉じられたり、ロックが解除されたりするのを信頼できる。JavaScriptは他の場所でも同期と非同期を明確にしてるし、ここも例外じゃない。メソッドはdisposeが完了するのを待たなきゃいけないから、もしdisposeが非同期なら、メソッドも非同期にしなきゃならないんだ。ただ、慣れてないとawait using a = await b()みたいにダブルawaitになるのはちょっと面倒かもね。シンボルを使うのは、時間をかけて追加された他の機能と同じで、イテレーターとかもそうだし、後方互換性を保ちながらサポートを追加するいい方法なんだ。シンボルを扱うのは主にライブラリの作者だけで、典型的なアプリ開発者は直接触れる必要はほとんどないよ。

ガベージコレクトされる言語では、デストラクタはほとんどの場合同期的に呼び出せないんだ。VMがまずオブジェクトにアクセスできないことを確認しなきゃいけないからね。だから、あまり決定的に動作しないし、JS VMの内部を露呈しちゃう。JSにはすでにWeakRefやFinalizationRegistryがあるし。https://waspdev.com/articles/2025-04-09/features-that-every-... https://waspdev.com/articles/2025-04-09/features-that-every-... でも、Mozillaですらそれらを使うことはあまり推奨してないよ。予測不可能で、エンジンによって動作が異なることがあるからね。

JavaScriptは未開発だからね。

デストラクタは決定的なクリーンアップが必要だけど、高度なガベージコレクション(GC)ではそれができないし、効率の観点からもあまりやりたくないんだよね。高度なGCを持つ言語では、コレクション中に呼ばれる「ファイナライザ」があって、これがすごく信頼性が低い(しかも微妙な罠がいっぱいある)んだ。通常はネイティブリソース(FFIラッパー)のための最終手段として使われることが多い。だから、多くの言語はスコープベースのリソースクリーンアップの手段を持っていたり、最終的にそれを成長させたりしているんだ。例えば、 - 高階関数ベース(Smalltalk、Haskell、Ruby) - 専用のスコープ/値フック(Python、C#、Java) - コールバック登録(Go、Swift) [1]: Pythonは元々、参照カウントGCのおかげでデストラクタを使ってたけど、参照カウントされていない実装や参照カウントのサイクル、ロックのようなリソースにガードがないこと(明確なユーティリティがないのにそれを追加したくない)から、コンテキストマネージャが導入されたんだ。

JavaScriptには匿名プロパティなんて存在しないよ。君の質問は意味がわからない。他に何が考えられるっていうの?

C#を思い出すなぁ.. C#のIDisposableとIAsyncDisposableは、ロック処理やキューのメカニズム、一時的なスコープの扱いなど、実際にきれいに抽象化すべきものを書くのにすごく役立つよね。

これは基本的にC#からの流用で、元の提案はそれを隠していないし、PythonのコンテキストマネージャやJavaのリソースを使ったtry、C#のusing文やusing宣言をすべて引用しているよ。そして、usingがキーワードで、disposeがフックメソッドっていうのはかなりのヒントだね。

それは提案の著者がMicrosoft出身で、C#と違う構文に見える反対提案を何度も却下してきたからだよ。 https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen...

彼らの最初の例は、こういう関数の中でtry/finallyブロックを持つ必要があるってことだね:function processData(response) { const reader = response.body.getReader(); try { reader.read() } finally { reader.releaseLock(); } } だから、reader.read()がエラーを投げてもロックが解除されるようになってる。これは長時間実行されるプロセスにだけ当てはまるのかな?ブラウザ環境やエラーが投げられたときに終了するCLIスクリプトでは、プロセスが終了するときにロックは解除されるの?

仕様には、ブロックが「完了」したとき、つまりどういう形であれ(通常の完了、例外、break/continue文など)、disposeが実行されなきゃいけないって書いてあるんだ。これは「using」にも「try/finally」にも同じことが言えるよ。プロセスが強制終了されたときの動作は、ECMAScript仕様の範囲外だから、その時点でインタープリターはこれ以上のアクションを取れなくなるんだ。だから、何が起こるかは話しているオブジェクトの種類による。記事の例は、ウェブプラットフォームのストリーム仕様からの「ストリーム」について話してる。ここでのストリームは、JSインタープリター内にだけ存在するJSオブジェクトなんだ。JSインタープリターが消えたら、ロックがかかっているかどうかを問うのは意味がないよ、だってロック自体が存在しなくなるから。もしOSが割り当てたリソース(例えば、割り当てられたメモリやファイルディスクリプタ)について話しているなら、プロセスが終了するときには一般的にOSが提供するクリーンアップが行われるよ。終了の仕方に関係なく、プロセス自体が何もしなくてもね。でも、もちろん詳細はプラットフォームに依存するけど。

Hacker Newsで議論の続きを見る