Get to know MDN better
このページはコミュニティーの尽力で英語から翻訳されました。MDN Web Docs コミュニティーについてもっと知り、仲間になるにはこちらから。
このガイドでは、JavaScript でリソース管理を行う方法について説明します。リソース管理は、より高度なトピックであり、通常は JavaScript によって自動的に処理されるメモリー管理とまったく同じでは ありません。リソース管理とは、JavaScript によって自動的にはクリーンアップされないリソースを管理することです。アプリケーションのロジックに支障をきたさない限り、メモリーに未使用のオブジェクトを保持していても問題ない場合もありますが、リソースリークは多くの場合、動作不良やメモリー使用量の過剰な増加につながります。したがって、これは単なる最適化のためのオプション機能ではなく、正しいプログラムを書くための核心的な機能なのです。
メモ: メモリー管理とリソース管理は別個のトピックですが、最終手段として、メモリー管理システムを利用してリソース管理するのが最適です。例えば、外部リソースのハンドルを表す JavaScript オブジェクトがある場合、そのハンドルがガベージコレクションされた際にリソースをクリーンアップするために FinalizationRegistry を作成することができます。なぜなら、その後そのリソースにアクセスする手段は確実に存在しなくなるからです。ただし、ファイナライザーが確実に実行されるとは限らないため、重要なリソースについてはこれに依存するのは得策ではありません。
まず、管理する必要があるリソースの例をいくつか見ていきましょう。
ファイルハンドル: ファイルハンドルは、ファイル内のバイト列を読み書きするために使用されます。使い終わったら、fileHandle.close() を呼び出す必要があります。そうしないと、JS オブジェクトにアクセスできなくなった後もファイルが開いたままになってしまいます。リンク先の Node.js ドキュメントにも次のように記載されています。
<FileHandle> を fileHandle.close() メソッドで閉じなかった場合、ファイル記述子は自動的に閉じられようとするため、プロセス警告が出力されます。これにより、メモリーリークの防止に役立ちます。ただし、この動作は信頼性が低く、ファイルが閉じられない可能性があるため、この動作に依存しないでください。代わりに、常に <FileHandle> を明示的に閉じてください。Node.js では、将来この動作が変更される可能性があります。
ネットワーク接続:WebSocket や RTCPeerConnection などの一部のコネクションでは、メッセージが送信されない場合、閉じなければならない場合があります。そうしないと接続が開いたままになり、接続プールのサイズはとても制限されているためです。
ストリームリーダー: ReadableStreamDefaultReader.releaseLock() を呼び出さなかった場合、ストリームはロックされ、他のリーダーが消費することができなくなります。
読み取り可能なストリームを使用した具体的な例を挙げます。
ここでは、3 つのデータチャンクを出力するストリームがあります。文字 "b" を探すまで、このストリームから読み込みを行います。readUntil から戻った時点で、ストリームは部分的にしか消費されていないため、別のリーダーを使用して読み込みを続けることができるはずです。しかし、ロックの解放を忘れてしまったため、reader は利用できなくなりました。しかし、ストリームはロックされたままであり、別のリーダーを作成することができません。
この場合の解決策は単純明快です。readUntilの最後にreader.releaseLock()を呼び出せばよいのです。しかし、まだいくつかの課題が残っています。
一貫性がないこと: リソースごとに解放方法が異なります。例えば、close()、releaseLock()、disconnect() などがあります。このパターンは一般化できません。
エラー処理: reader.read() の呼び出しが失敗した場合はどうなるでしょうか?その場合、readUntil は終了してしまい、reader.releaseLock() の呼び出しには決して到達しません。これは try...finally を使って対処できます。
ただし、重要なリソースを公開するたびに、この作業を行う必要があることを覚えておく必要があります。
スコープについて: 上記の例では、reader は try...finally 文を終了した時点ですでに閉じられていますが、そのスコープ内では引き続き利用できます。つまり、閉じられた後に誤って使用してしまうことがあります。
複数のリソース: 異なるストリーム上に 2 つのリーダーを持つ場合、両方を解放することを忘れないようにしなければなりません。これは、そのための試みです。
しかし、このことによってエラー処理の複雑さが増します。もし stream2.getReader() で例外が発生した場合、reader1 は解放されません。また、reader1.releaseLock() で例外が発生した場合、reader2 は解放されません。つまり、実際にはそれぞれのリソースの取得と解放のペアを、それぞれ独自の try...finally ブロックで囲む必要があります。
releaseLock を呼び出すという、一見単純な作業が、すぐに複雑に絡み合った入れ子状の定型コードにつながってしまうことがお分かりいただけるでしょう。だからこそ、JavaScript ではリソース管理のための言語レベルでのサポートが提供されているのです。
用意されている解決策は、2種類の特別な変数宣言、using と await using です。これらは const に似ていますが、リソースが破棄可能である場合、変数スコープ外に出た際に自動的にリソースを解放します。前述の例を用いて、次のように書き換えることができます。
メモ: この記事の執筆時点では、ReadableStreamDefaultReader は破棄可能プロトコルを実装していません。これはあくまで仮定の例です。
まず、コードを囲む追加の波括弧に注目してください。これにより、using 宣言のための新しい ブロックスコープ が作成されます。using で宣言されたリソースは、using のスコープ外に出たときに自動的に解放されます。この場合、スコープ外に出るタイミングは、すべての文が実行されたとき、あるいはどこかでエラーや return/break/continue に遭遇したときなど、ブロックを終了するときです。
つまり、using は明確な有効期間を持つスコープ内でのみ使用できます。すなわち、スクリプトの最上位では使用できません。なぜなら、スクリプトの最上位にある変数は、そのページ上の今後のすべてのスクリプトにおいてスコープ内にあるため、ページがアンロードされない限り、実質的にそのリソースは解放されないことになるからです。ただし、モジュールの最上位では使用可能です。モジュールのスコープは、モジュールの実行が完了すると終了するためです。
これで、using がいつクリーンアップを行うかがわかりました。では、どのように行われるのでしょうか。using を使用するには、リソースが破棄可能プロトコルを実装している要求されます。オブジェクトが [Symbol.dispose]() メソッドを保有している場合、そのオブジェクトは破棄可能です。このメソッドは引数なしで呼び出され、クリーンアップを実行します。例えば、リーダーの場合、[Symbol.dispose] プロパティは releaseLock の単純な別名やラッパーにすることができます。
この破棄プロトコルにより、using は、リソースの種類を把握することなく、すべてのリソースを一貫した方法で破棄することができます。
各スコープには、宣言された順序で関連付けられたリソースのリストがあります。スコープが終了すると、リソースは [Symbol.dispose]() メソッドを呼び出すことで、逆順で破棄されます。例えば、上記の例では、reader1 が reader2 よりも前に宣言されているため、reader2 がまず破棄され、次に reader1 が破棄されます。あるリソースの破棄を試みた際に発生するエラーは、他のリソースの破棄を妨げることはありません。これは try...finally パターンと整合しており、リソース間の依存関係を考慮した設計となっています。
await using は using とよく似ています。この構文は、await がどこかで現れることを指示しています。つまり、リソースが宣言されたときではなく、実際に破棄される際に現れるということです。await using を使用するには、リソースが非同期に破棄可能である必要があります。つまり、[Symbol.asyncDisposable]() メソッドを持っている必要があります。このメソッドは引数なしで呼び出され、クリーンアップが完了したときにプロミスを返します。これは、fileHandle.close() のようにクリーンアップが非同期である場合に有益です。この場合、破棄の結果は非同期にしか確認できません。
await using は await の実行が要求されるため、await が許可されているコンテキストでのみ使用できます。これには、async 関数内や、モジュール内の最上位での await が含まれます。
リソースのクリーンアップは並行して行われるのではなく、順次行われます。つまり、あるリソースの [Symbol.asyncDispose]() メソッドの返値が await されるまで、次のリソースの [Symbol.asyncDispose]() メソッドは呼び出されません。
注意点:
using と await using は特別な構文です。構文は便利で、複雑さの多くを隠してくれますが、時には手動で行う必要がある場合もあります。
例えば、リソースをこのスコープの終了時に破棄するのではなく、それより後のスコープで破棄したい場合はどうでしょうか。次のようなケースを考えてみてください。
前述の通り、using は const と同様に、初期化する必要があり、再代入することはできないので、次のように書こうとするかもしれません。
しかし、これではすべてのロジックを if や else の内部に記述しなければならず、コードの重複が発生してしまいます。私たちが実現したいのは、あるスコープでリソースを取得・登録し、別のスコープで破棄することです。そのためには DisposableStack を使用することができます。これは、破棄可能なリソースの集合を保持し、それ自体が破棄可能なオブジェクトです。
まだ破棄可能プロトコルを実装していないリソースがある場合、using はそれを受け付けません。その場合は、adopt() を使用することができます。
特定の資源に「紐づけられて」いないが、実行すべき破棄処理を設定したい場合もあります。たとえば、複数の接続が同時に開かれている際に、「すべてのデータベース接続が閉じられました」というメッセージをログに出したい場合などが挙げられます。このような場合、defer() を使用することができます。
条件付きでの破棄をしたい場合があるかもしれません。例えば、エラーが発生した場合にのみ、割り当てられたリソースを破棄するなどです。その場合は、move() を使用することで、通常であれば破棄されるはずのリソースを保持することができます。
AsyncDisposableStack は DisposableStack と似ていますが、非同期の破棄可能リソースを使用するためのものです。その use() メソッドは非同期の破棄可能オブジェクトを受け取り、adopt() メソッドは非同期のクリーンアップ関数を受け取り、dispose() メソッドは非同期のコールバックを受け取ります。また、[Symbol.asyncDispose]() メソッドも提供しています。同期リソースと非同期リソースが混在している場合でも、同期リソースを渡すことが可能です。
DisposableStack のリファレンスには、他にも例と詳細があります。
リソース管理機能の主な用途は、エラーが発生した場合でも、リソースが常に解放されるように実現することです。ここでは、いくつかの複雑なエラー処理のシナリオについて見ていきましょう。
まず、using を使用することでエラーに対して堅牢な、以下のコードから始めます。
chunk が null だったと仮定します。その場合、!chunk.done は TypeError を発生させ、関数が終了します。関数が終了する前に、stream[Symbol.dispose]() が呼び出され、ストリームのロックが解放されます。
つまり、using はエラーを隠蔽しません。発生したエラーはすべて送出されますが、その直前にリソースは閉じられます。では、リソースのクリーンアップ処理自体がエラーを送出した場合はどうなるでしょうか。もう少し極端な例を見てみましょう。
doSomething() の呼び出しで2つのエラーが発生しています。1 つは doSomething の実行中に送出されたエラー、もう 1 つは最初のエラーが原因で reader の破棄中に送出されたエラーです。これら 2 つのエラーは同時に送出されるため、捕捉されたものは SuppressedError となります。これは 2 つのエラーを内包する特殊なエラーであり、error プロパティには後者のエラーが、suppressed プロパティには前者のエラーが含まれていて、前者のエラーは後者のエラーによって「抑制」されています。
リソースが複数あり、その両方が破棄中にエラーを発生した場合(これは極めて稀なケースであるはずだです。そもそも破棄に失敗すること自体が稀であるためです)、それぞれの先行するエラーは後続のエラーによって抑制され、抑制されたエラーの連鎖が形成されます。
次の例では、Blob のオブジェクト URL を作成し(実際のアプリケーションでは、この Blob はファイルやフェッチレスポンスなどから取得されることになります)、Blob をファイルとしてダウンロード可能にします。リソースリークを防ぐため、オブジェクトURLが不要になった時点(つまり、ダウンロードが正常に始まった時点)で、URL.revokeObjectURL() を使用してオブジェクトURLを解放しなければなりません。URL 自体は単なる文字列であり、破棄可能プロトコルを実装していないため、url を using で直接宣言することはできません。そのため、url のディスポーザーとして機能する DisposableStack を作成します。オブジェクト URL は、link.click() が完了するか、どこかでエラーが発生したかした時点で disposer がスコープ外になるとすぐに破棄されます。
次の例では、リソースのリストを並行して fetch で読み取るために Promise.all() を使用しています。Promise.all() は、1 つのリクエストが失敗すると直ちに失敗し、結果のプロミスを拒否します。しかし、他の待機中のリクエストは、プログラムからその結果にアクセスできなくなっても、実行され続けます。これらの残りのリクエストが不必要にリソースを消費するのを避けるには、Promise.all() が決定した際に、進行中のリクエストを自動的にキャンセルする必要があります。キャンセル処理は AbortController を使用して実装し、その signal をすべての fetch() 呼び出しに渡します。Promise.all() が履行された場合、関数は通常通り戻り、コントローラーは中止されます。この時点ではキャンセルすべき待機中のリクエストが存在しないため、問題はありません。一方、Promise.all() が拒否され、関数が例外を送出した場合、コントローラーは中止され、すべての待機中のリクエストがキャンセルされます。
リソース解放の構文には、どのような状況でもリソースが常に解放されるよう強力なエラー処理機能が数多く備わっていますが、それでも遭遇しうる落とし穴があります。
リソース管理機能は万能薬ではありません。手動で破棄メソッドを呼び出すよりも確実に改善されていますが、リソース管理に関するすべてのバグを防ぐほど賢くはありません。使用するリソースの仕様を十分に理解し、注意を払う必要があります。
リソース管理システムの主要な成分は以下の通りです。
これらの API を適切に使用すれば、多くの定型コードを記述することなく、あらゆるエラー状況に対しても堅牢で信頼性の高い、外部リソースと対話するシステムを生成できます。
This page was last modified on 2026年3月17日 by MDN contributors.
Your blueprint for a better internet.
Visit Mozilla Corporation’s not-for-profit parent, the Mozilla Foundation.
Portions of this content are ©1998–2026 by individual mozilla.org contributors. Content available under a Creative Commons license.