すべての JavaScript 開発者が少なくとも一度は書いたことがあるでしょう:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

// Later…
eventBus.on('done', () => resolve(result));

Enter fullscreen mode Exit fullscreen mode

Promise をコンストラクタの外からトリガーする必要があります — イベントリスナー、コールバック、またはメッセージハンドラーから。そこで resolve と reject を外側のスコープにリークさせてキャプチャします。これは動作します。しかし、逃げることを意図していないクロージャから手動で脱出しているため、書くたびに間違っているように見えます。

ES2024 はこのための正しいツールを追加しました: Promise.withResolvers です。

Promise.withResolvers が返すもの

Promise を構築してコールバックを脱出させる代わりに、3 つの要素を一度に渡してくれる静的メソッドを呼び出します:

const { promise, resolve, reject } = Promise.withResolvers();

Enter fullscreen mode Exit fullscreen mode

promise は通常の Promise です。resolvereject はその解決関数で、すでにあなたのスコープに存在しています。クロージャの脱出は必要ありません。以前は 4 行必要だったパターンが、今では 1 行で済みます。

古いパターンと新しいパターンはまったく同じように動作します — 同じマイクロタスクのタイミング、同じエラー伝播、同じ .then/.catch インターフェース。違いは、意図が明示的になったことです: 回避策としてではなく、意図的に deferred Promise を作成しているということです。

イベントから Promise への橋渡し

最もわかりやすいユースケースは、イベントベースの API を Promise インターフェースでラップすることです。以前:

function waitForOpen(socket) {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  socket.addEventListener('open', () => resolve());
  socket.addEventListener('error', (e) => reject(e));
  return promise;
}

Enter fullscreen mode Exit fullscreen mode

After:

function waitForOpen(socket) {
  const { promise, resolve, reject } = Promise.withResolvers();
  socket.addEventListener('open', () => resolve());
  socket.addEventListener('error', (e) => reject(e));
  return promise;
}

Enter fullscreen mode Exit fullscreen mode

ロジックは同じで、手続きは大幅に少なくなりました。この関数はソケットのイベントに基づいて解決または拒否される Promise を返します — まさにクリーンなバージョンから読み取れる内容です。古いバージョンでは、そこに到達するために脱出用の定型コードを解析する必要がありました。

非同期キューとメッセージパッシング

deferred Promise が繰り返し登場する場所の 1 つが非同期キューです — コンシューマーが次のアイテムを待機し、プロデューサーが後で push を呼び出す構造です:

function createQueue() {
  const pending = [];
  let deferred = Promise.withResolvers();

  return {
    push(item) {
      pending.push(item);
      deferred.resolve();
      deferred = Promise.withResolvers();
    },

    async *[Symbol.asyncIterator]() {
      while (true) {
        await deferred.promise;
        while (pending.length) {
          yield pending.shift();
        }
      }
    },
  };
}

const queue = createQueue();

// Consumer
(async () => {
  for await (const item of queue) {
    console.log('received:', item);
  }
})();

// Producer (elsewhere)
queue.push('hello');
queue.push('world');

Enter fullscreen mode Exit fullscreen mode

Promise.withResolvers() を呼び出すたびに新しいゲートが作成されます。コンシューマーはそれを待機し、プロデューサーは読み取るべきものがあるときにそれを解決します。withResolvers がなければ、これらのゲートのすべてに脱出用の定型コードが必要になります。それがあれば、キューのロジックはストレートに読み進められます。

制御されたフラッシュ: バッチを待機する

もう 1 つのパターン: バッチ操作のためにアイテムを収集し、バッチが実行されたときにすべての待機者を一度に解決する。

class Batcher {
  #items = [];
  #deferred = Promise.withResolvers();

  add(item) {
    this.#items.push(item);
    return this.#deferred.promise;
  }

  async flush() {
    const batch = this.#items.splice(0);
    const { resolve } = this.#deferred;
    this.#deferred = Promise.withResolvers();
    const results = await this.#processBatch(batch);
    resolve(results);
    return results;
  }
}

Enter fullscreen mode Exit fullscreen mode

呼び出し元は await batcher.add(item) を実行し、バッチがフラッシュされたときに結果を取得します — タイマー、サイズ制限、または明示的なユーザーアクションによってトリガーされる場合があります。各フラッシュは単一の Promise.withResolvers() 呼び出しでゲートをクリーンにリセットします。

使わない方がよい場合

Deferred Promise は特定の状況のためのツールであり、コンストラクタ形式の一般的な代替ではありません。セットアップと解決の両方を制御できる場合 — fetchfs.readFile、直接待機している任意の非同期 API — では、async/await または Promise コンストラクタを直接使用してください。Deferred パターンは、セットアップと解決が異なる実行コンテキストで発生し、コールバックをクリーンに共有できない場合に特化しています。

具体的なアンチパターンの 1 つ: deferred を作成した同じ関数内で同期的に resolve を呼び出している場合、おそらく new Promise(res => res(value)) が欲しかったのでしょう — これは Promise.resolve(value) です。

TypeScript サポート

TypeScript はバージョン 5.4 で Promise.withResolvers を追加しました。戻り値の型は正しく型付けされています — PromiseWithResolvers<T> が型を維持します:

const { promise, resolve, reject } = Promise.withResolvers<string>();

resolve('hello');       // ✅ string
resolve(42);            // ❌ type error

Enter fullscreen mode Exit fullscreen mode

古い TS ターゲットを使用している場合は、tsconfig.jsonlib"ES2024" を追加してください。

ブラウザサポート

Promise.withResolversBaseline 2024 です: Chrome 119、Firefox 121、Safari 17.4、Node.js 22。モダンな環境をターゲットにしている場合、ポリフィルなしで利用可能です。古いターゲットの場合、古い脱出パターンが正しいままです — withResolvers は手動で常に書けるもののシンタックスシュガーです。

まとめ

コードベースで let resolvelet reject に続いて new Promise というパターンを検索してください。それらはすべて、難しい方法で書かれた deferred Promise です。Promise.withResolvers() はパターンに名前を付け、脱出用の定型コードを排除し、意図を一目で読みやすくします。結果は同じですが、コードは目に見えてクリーンになります。


Thanks for reading! Let's stay connected: