JavaScript Promise.withResolvers()で非同期コードをすっきり整理する

問題

イベントベースのAPIをラップしたり、タイマーをPromiseに変換する際、Promiseを手動で作成する必要があります。

従来のアプローチはこのようになります:

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

// resolve/rejectを外部で使用
someEmitter.on('done', resolve);
someEmitter.on('error', reject);

resolverejectを外部に取り出すためにletで宣言してコールバック内で代入するパターンです。 動作はしますが、少し不格好ですよね。

解決方法

Promise.withResolvers()を使えば、これを1行で解決できます。

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

someEmitter.on('done', resolve);
someEmitter.on('error', reject);

分割代入でpromiseresolverejectの3つを一度に取得できます。

実務でよく使うパターンをもう一つご紹介します:

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

  const timeout = setTimeout(() => {
    reject(new Error('Request timeout'));
  }, 5000);

  return {
    promise,
    complete(data) {
      clearTimeout(timeout);
      resolve(data);
    },
    fail(error) {
      clearTimeout(timeout);
      reject(error);
    }
  };
}

// 使用例
const req = createDeferredRequest();
ws.send(message);
ws.onmessage = (e) => req.complete(JSON.parse(e.data));
const result = await req.promise;

WebSocketのようなイベントベースの通信で、リクエスト・レスポンスパターンを構築する際にとても便利です。

ポイント

  • Promise.withResolvers(){ promise, resolve, reject }オブジェクトを返します
  • コンストラクタのコールバックなしで、直接resolve/rejectの参照を取得できます
  • Chrome 119+、Firefox 121+、Safari 17.4+、Node.js 22+でサポートされています
  • Deferredパターンが必要なあらゆる場面で活用できます