すべての 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 です。resolve と reject はその解決関数で、すでにあなたのスコープに存在しています。クロージャの脱出は必要ありません。以前は 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 は特定の状況のためのツールであり、コンストラクタ形式の一般的な代替ではありません。セットアップと解決の両方を制御できる場合 — fetch、fs.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.json の lib に "ES2024" を追加してください。
ブラウザサポート
Promise.withResolvers は Baseline 2024 です: Chrome 119、Firefox 121、Safari 17.4、Node.js 22。モダンな環境をターゲットにしている場合、ポリフィルなしで利用可能です。古いターゲットの場合、古い脱出パターンが正しいままです — withResolvers は手動で常に書けるもののシンタックスシュガーです。
まとめ
コードベースで let resolve や let reject に続いて new Promise というパターンを検索してください。それらはすべて、難しい方法で書かれた deferred Promise です。Promise.withResolvers() はパターンに名前を付け、脱出用の定型コードを排除し、意図を一目で読みやすくします。結果は同じですが、コードは目に見えてクリーンになります。
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): [email protected]
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.