6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】usingがStage4になったので、ようやくリソース解放忘れから解放されるよ

6
Last updated at Posted at 2026-08-31
だめ
function hoge(){
  const handle = exampleResource(); // なんかのリソース
  /*
    なんか長い処理
  */
  handle.release(); // リソース解放
}

hoge();

うっかり長い処理の途中でreturnされたり例外が発生したりしたら、リソースが解放されないまま残ってしまいます。
ということでtry-finallyで囲む書き方が定石になりました。

OKだけどめんどい
function hoge(){
  const handle = exampleResource();
  try{
    /*
      なんか長い処理
    */
  }finally{
    handle.release(); // リソース解放
  }
}

hoge();

まー面倒臭いですよね。

これは単体だからまだいいですが、関連する複数のリソースを順番に並べないといけないなどの要素が入ってくると、非常にややこしいコードになってしまいます。
ただ単にリソース解放したいだけなのに。

ということでもっと便利にリソース管理できるusingキーワードが導入されました。

かんたん!
function hoge(){
  using handle = exampleResource();
  /*
    なんか長い処理
  */
}

hoge();

スコープを抜けた時点で、自動的にリソースが解放されます。
C#とかのusingそのまんまの機能です。

現在のステータスはStage4で、すなわち既に複数のブラウザに実装されている状態です。

Firefoxは2025/07/22リリースのFirefox141で、Chromeは2025/03/04リリースのChrome134で既に対応済です。
そしてSafariも2025/08/13のプレビュー版TP250で対応したので、そのうち正式版も対応することでしょう。
めでたく全ブラウザで使用可能になります。

以下は該当のproposal、ECMAScript Explicit Resource Managementの紹介です。

ECMAScript Explicit Resource Management

このproposalは、メモリやIOなど様々なリソースのライフサイクルに対する、一般的なパターンを改善することを目的としています。
そのパターンとは、リソースの割り当てと解放の機能です。

たとえばジェネレータ関数は、returnを呼び出すとfinallyブロックのクリーンアップが実行されるようになっています。

同期ジェネレータ
function * g() {
  const handle = acquireFileHandle(); // 重要なリソース
  try {
    ...
  }
  finally {
    handle.release(); // リソース解放
  }
}

const obj = g();
try {
  const r = obj.next();
  ...
}
finally {
  obj.return(); // finallyが呼ばれる
}
非同期ジェネレータ
async function * g() {
  const handle = acquireStream(); // 重要なリソース
  try {
    ...
  }
  finally {
    await stream.close(); // リソース解放
  }
}

const obj = g();
try {
  const r = await obj.next();
  ...
}
finally {
  await obj.return(); // finallyが呼ばれる
}

本提案では、この一般的なパターンを簡略化するために新たな構文を導入します。

同期ジェネレータ
function * g() {
  using handle = acquireFileHandle(); // ブロックスコープ
} // handleがクリーンアップされる

{
  using obj = g(); // ブロックスコープ
  const r = obj.next();
} // finallyが呼ばれる
非同期ジェネレータ
async function * g() {
  using handle = acquireFileHandle(); // ブロックスコープ
} // handleがクリーンアップ

{
  await using obj = g(); // ブロックスコープ
  const r = await obj.next();
} // finallyが呼ばれる

また複数のリソース状態を管理するため、ふたつのコンテナオブジェクトを追加します。
・DisposableStack 使い捨てリソースを格納するスタック
・AsyncDisposableStack 非同期の使い捨てリソースを格納するスタック

Motivations

このproposalは、以下のような事例を解決するためのものです。

Inconsistent patterns for resource management

一貫性のないリソース管理。

イテレータ:iterator.return()
ストリームリーダ:reader.releaseLock()
Nodeのファイルハンドラ:handle.close()
EmscriptenのC++オブジェクトハンドラ:Module._free(ptr)obj.delete()Module.destroy(obj)

Avoiding common footguns when managing resources

リソース管理のよくある落とし穴。

const reader = stream.getReader();
...
reader.releaseLock(); // finallyに入れるべき

Scoping resources

スコープ。

const handle = ...;
try {
  ... // ここではhandleを使える
}
finally {
  handle.close();
}
// ここではhandleを使えないのに、定義は残ってる

Avoiding lengthy code when managing multiple resources correctly

複数のリリース管理は煩雑になりがち。

{ // bはaに依存しているとする
  const a = ...;
  try {
    const b = ...;
    try {
      ...
    }
    finally {
      b.close(); // bを先に閉じる
    }
  }
  finally {
    a.close(); // b.close()がエラーになった場合でも動くように
  }
}
こう書けるようになる
using a = ..., b = ...;
...

Non-blocking memory/IO applications

ノンブロッキングIOアプリ。

import { ReaderWriterLock } from "...";
const lock = new ReaderWriterLock();

export async function readData() {
  // 書き込み完了を待って、読み取りロック
  using lockHandle = await lock.read();
  ... 
  await ...;
  ... // まだ読み取りロックが残ってる
} // 読み取りロックが解放される

export async function writeData(data) {
  // 読み取り完了を待って、書き込みロック
  using lockHandle = await lock.write();
  ... 
  await ...;
  ... // まだ書き込みロックが残ってる
} // 書き込みロックが解放される

Potential for use with the Fixed Layout Objects Proposal and shared struct

Structsのproposalとのシナジー。

main.js
// main.js
shared struct class SharedData {
  ready = false;
  processed = false;
}

const worker = new Worker('worker.js');
const m = new Atomics.Mutex();
const cv = new Atomics.ConditionVariable();
const data = new SharedData();
worker.postMessage({ m, cv, data });

// workerに送信
{
  // ロックmを取得するまで待機
  using lck = m.lock();

  data.ready = true;
  console.log("main is ready");

} // mは自動でアンロック

// 待機しているworkerに通知
cv.notifyOne();

{
  // ロックmを再度取得
  using lck = m.lock();

  // mをアンロックし、workerの完了まで待機
  cv.wait(m, () => data.processed);

} // mは自動でアンロック
worker.js
onmessage = function (e) {
  const { m, cv, data } = e.data;

  {
    // ロックmを取得するまで待機
    using lck = m.lock();

    // mをアンロックし、データが来るまで待機
    cv.wait(m, () => data.ready);

    // ロックmを取得した
    console.log("worker thread is processing data");

    // mainにデータ送信
    data.processed = true;
    console.log("worker thread is done");

  } // mは自動でアンロック
}

Prior Art

既存の実装。

・C#のusing
・Javaのtry-with-resources
・Pythonのwith

Semantics

using Declarations

同期バインディング。

UsingDeclaration :
  `using` BindingList `;`

LexicalBinding :
    BindingIdentifier Initializer

パーサーはusingを見つけると、そのバインディングはブロックを抜けたときに破棄するようにマークされます。
したがってusingはスクリプトの最上位で宣言することはできません。

{
  ... // (1)
  using x = expr1;
  ... // (2)
}

上記は、以下の実行時セマンティクスを持ちます。

{
  const $$try = { stack: [], error: undefined, hasError: false };
  try {
    ... // (1)

    const x = expr1;
    if (x !== null && x !== undefined) {
      const $$dispose = x[Symbol.dispose];
      if (typeof $$dispose !== "function") {
        throw new TypeError();
      }
      $$try.stack.push({ value: x, dispose: $$dispose });
    }

    ... // (2)
  }
  catch ($$error) {
    $$try.error = $$error;
    $$try.hasError = true;
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
        $$try.hasError = true;
      }
    }
    if ($$try.hasError) {
      throw $$try.error;
    }
  }
}

using Declarations with Multiple Resources

1つのusingで、複数のリソースをバインドすることができます。

{
  ...
  using x = expr1, y = expr2;
  ...
}

ブロックもしくはモジュールを抜けた際にリソースが解放されますが、これは宣言とは逆の順番で呼び出されます。
以下のコードとほぼ同じです。

{
  ... // (1)
  using x = expr1;
  using y = expr2;
  ... // (2)
}

上記は、以下の実行時セマンティクスを持ちます。

{
  const $$try = { stack: [], error: undefined, hasError: false };
  try {
    ... // (1)

    const x = expr1;
    if (x !== null && x !== undefined) {
      const $$dispose = x[Symbol.dispose];
      if (typeof $$dispose !== "function") {
        throw new TypeError();
      }
      $$try.stack.push({ value: x, dispose: $$dispose });
    }

    const y = expr2;
    if (y !== null && y !== undefined) {
      const $$dispose = y[Symbol.dispose];
      if (typeof $$dispose !== "function") {
        throw new TypeError();
      }
      $$try.stack.push({ value: y, dispose: $$dispose });
    }

    ... // (2)
  }
  catch ($$error) {
    $$try.error = $$error;
    $$try.hasError = true;
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
        $$try.hasError = true;
      }
    }
    if ($$try.hasError) {
      throw $$try.error;
    }
  }
}

リソースを適切に解放することを保証するため、初期化中に異常が発生した場合でも正常にクリーンアップが実行されるようにする必要があります。
一括で複数の宣言をした場合、リソースは宣言した順で初期化され、解放は逆順で行われます。

using Declarations and null or undefined Values

本proposalでは、usingnullundefinedを渡すことが許容され、単に無視されます。
これはC#のusingと同じような動作です。
理由のひとつとして、リソースがオプションである場合の書式を簡素化するためであり、これによって余計な書式や不要なメモリ割り当てが不要になります。

if (isResourceAvailable()) {
  using resource = getResource();
  ... // (1)
  resource.doSomething()
  ... // (2)
}
else {
  // こっちにも同じようなことを書く必要がある
  ... // (1) 
  ... // (2) 
}

// これでよくなる
using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) リソースがあってもなくても実行
resource?.doSomething();
... // (2) リソースがあれば実行

using Declarations and Values Without [Symbol.dispose]

リソースに[Symbol.dispose]が存在しない場合、追跡対象にしようとした時点でTypeErrorが発生します。

using Declarations in for-of and for-await-of Loops

for-offor-await-ofループの変数宣言にusingを記述することができます。

for (using x of iterateResources()) {
  // xを使う何か
}

各ループでバインドされた値は、各ループが終わるたびに破棄されます。
ただしreturnやbreak・throwなどでループを離脱した場合は破棄されません。

for-inループではusingを使うことはできません。

await using Declarations with Explicit Local Bindings

非同期バインディング。

{
  ... // (1)
  await using x = expr1;
  ... // (2)
}

await usingで生成されたバインドは、それを含む非同期関数やブロックの終わりで破棄されるよう追跡されます。

以下await usingの定義などが書かれているのですが、awaitのつかない方のusingとほぼ同じなので省略。

await using Declarations and Values Without [Symbol.asyncDispose] or [Symbol.dispose]

リソースに[Symbol.dispose][Symbol.asyncDispose]のいずれも存在しない場合、追跡対象にしようとした時点でTypeErrorが発生します。

Implicit Async Interleaving Points ("implicit await")

await usingは、宣言を含んだ非同期関数やブロックから抜けるときに、暗黙的な非同期インターリーブが導入されます。
すなわち、暗黙的にawaitが実行されます。

async function f() {
  {
    a();
  } // exit block
  b(); // aと同じマイクロタスク
}

現在は普通に書くとb()a()と同じマイクロタスク内で実行されますが、await usingが導入されると別のタスクになります。

async function f() {
  {
    await using x = ...;
    a();
  } // exit block
  b(); // aとは別のマイクロタスク
}

await using構文が暗黙的な非同期インターリーブにどのように関係するかについてはhttps://github.com/tc39/proposal-async-explicit-resource-management/issues/1を参照ください。

Example

以下は、このproposalが採用されたのちのAPIの使い方の例です。

WHATWG Streams API

{
  using reader = stream.getReader();
  const { value, done } = reader.read();
} // readerが解放される

NodeJS FileHandle

{
  using f1 = await fs.promises.open(s1, constants.O_RDONLY),
        f2 = await fs.promises.open(s2, constants.O_WRONLY);
  const buffer = Buffer.alloc(4092);
  const { bytesRead } = await f1.read(buffer);
  await f2.write(buffer, 0, bytesRead);
} // f2が解放されたあとf1が解放される

NodeJS Streams

{
  await using writable = ...;
  writable.write(...);
} // writable.end()が呼ばれ、結果が返ってくるまで待つ

Logging and tracing

// audit privileged function call entry and exit
function privilegedActivity() {
  using activity = auditLog.startActivity("privilegedActivity"); // activity開始
  ...
} // activity終了

Async Coordination

import { Semaphore } from "...";
const sem = new Semaphore(1); // 同時参加者は1人だけ

export async function tryUpdate(record) {
  using lck = await sem.wait(); // 1人になるまで待機
  ...
} // Semaphoreが解放され、次の参加者に通知がいく

Three-Phase Commit Transactions

// どちらか片方でも失敗したらロールバックする
async function transfer(account1, account2) {
  await using tx = transactionManager.startTransaction(account1, account2);
  await account1.debit(amount);
  await account2.credit(amount);

  // ここまで到達したら成功フラグ
  tx.succeeded = true;
} // commitかrollback待ち

Additions to Symbol

SymbolにプロパティdisposeasyncDisposeが追加されます。
disposeはusingの終わりに、asyncDisposeawait usingの終わりに呼び出されます。

interface SymbolConstructor {
  readonly asyncDispose: unique symbol;
  readonly dispose: unique symbol;
}

Built-in Disposables

このあたりに内部構造やdisposeを実装する側の情報などが乗っているのですが、実装する側になる人は少ないだろうしそんな人は原文を直接見るでしょうから(あと面倒)省略します。

感想

これでうっかりリソース解放忘れからの事故が無くなりますね。
やったね。

といっても、たとえばNodeのcrypto.Hashのクリーンアップはhash.digestなのですが、Disposeされたときにhash.digestが呼ばれるためにはNode側が頑張って実装しなければなりません。
優先度の高いところから徐々に対応されつつはあるものの、決して今すぐ全てのリソースで使えるというものではありません。
そして対応していないリソースをうっかりusingするとエラーになってしまいます。
少々残念ですね。

ちなみにPHPなら、特に何もしなくてもリソースへの参照がなくなった時点で勝手にリソースを解放してくれるよ。
やったね。

6
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?