Promise完全実践ガイド〜then/catch/finally・チェーン・並列・withResolvers・AbortController・パターン20選【2026年版】〜

Promise は ECMAScript 2015 で標準化された JavaScript の非同期処理プリミティブです。async/await はこの Promise の上に乗ったシンタックスシュガーに過ぎず、ライブラリ実装・並列制御・キャンセル・低レイヤ最適化を行う場面では、結局のところ Promise そのものをハンドリングする力が問われます。

本記事では ECMAScript 2025 / Node.js 22+ / TypeScript 5.x 準拠で、コピペで動く 40 以上の Promise コードサンプルを通して、new Promise による生成 → then/catch/finally によるチェーン → Promise.all / race / allSettled / any による並列制御 → ES2024 で標準化された Promise.withResolvers → AbortController によるキャンセル → queueMicrotask によるマイクロタスク制御 → 自前 Promise キュー実装 → p-limit 内部実装 → TypeScript 型付けまで、Promise をプリミティブとして使いこなす知識を体系的に整理します。

本記事は async/await 完全実践ガイド(高位抽象の観点)と JavaScript ベストプラクティス 10 選(言語全般)と相互補完的に読むことで、非同期処理の表層から内部実装まで一気通貫で押さえることができます。

  1. 1. Promise とは何か – 状態機械としての理解
    1. 1.1 Promise の 3 状態と不可逆な遷移
    2. 1.2 Promise.resolve / Promise.reject – 値からの生成
    3. 1.3 new Promise(executor) – executor 関数の正しい書き方
  2. 2. then / catch / finally – チェーンの本質
    1. 2.1 then は新しい Promise を返す
    2. 2.2 then の中で Promise を return すると平坦化される
    3. 2.3 catch は then(undefined, onRejected) の糖衣構文
    4. 2.4 finally は値を変更しない
  3. 3. エラー伝播 – チェーンの中での例外の流れ
    1. 3.1 エラーは catch まで素通りする
    2. 3.2 catch 後にチェーンを再開できる
    3. 3.3 catch 内で再 throw する
  4. 4. async/await との関係 – 内部実装の理解
    1. 4.1 await は then の同期的書き方
    2. 4.2 ライブラリは Promise で書く方が薄い
  5. 5. Promise.all – 並列実行の基本
    1. 5.1 配列の Promise を一括待ち
    2. 5.2 オブジェクト形式で並列取得(Promise.all + Object.entries)
    3. 5.3 短絡評価の罠 – 他の Promise はキャンセルされない
  6. 6. Promise.race / allSettled / any – 並列の使い分け
    1. 6.1 Promise.race – 最速の結果を取る
    2. 6.2 Promise.allSettled – 全件の結果を集める
    3. 6.3 Promise.any – 最初の成功を取る
    4. 6.4 4 メソッド比較表
  7. 7. Promise.withResolvers – ES2024 の新顔
    1. 7.1 基本: Deferred パターンの公式実装
    2. 7.2 イベントを Promise 化する
    3. 7.3 旧来の自前 Deferred 実装と比較
  8. 8. 非同期 API の Promise 化
    1. 8.1 setTimeout を Promise 化(sleep 関数)
    2. 8.2 FileReader を Promise 化
    3. 8.3 Node.js コールバック API の Promise 化(util.promisify)
    4. 8.4 自作 promisify を書く
  9. 9. リトライ・タイムアウト・並列数制限
    1. 9.1 リトライ実装(指数バックオフ)
    2. 9.2 タイムアウト実装(AbortController 連携)
    3. 9.3 並列数制限(p-limit 風の自前実装)
  10. 10. キャンセル – AbortController と Promise の連携
    1. 10.1 AbortController の基本
    2. 10.2 任意の Promise を AbortSignal でキャンセルできるようにする
    3. 10.3 AbortSignal.any() で複数 signal を合成(ES2024)
  11. 11. マイクロタスク – Promise の実行タイミング
    1. 11.1 マクロタスク vs マイクロタスク
    2. 11.2 queueMicrotask の使い所
    3. 11.3 requestAnimationFrame を Promise 化
  12. 12. unhandledrejection – 捕捉漏れの監視
    1. 12.1 ブラウザでの監視
    2. 12.2 Node.js での監視
    3. 12.3 catch を忘れがちなパターン
  13. 13. 自前 Promise キュー – 順次実行の制御
    1. 13.1 シンプルな順次キュー
    2. 13.2 デバウンス Promise(連打対策)
  14. 14. TypeScript での Promise 型付け
    1. 14.1 基本的な型注釈
    2. 14.2 Promise.all の型推論
    3. 14.3 Result 型でエラーを型レベルで扱う
  15. 15. Node.js での Promise 活用
    1. 15.1 fs/promises – ファイルシステム
    2. 15.2 timers/promises – setTimeout の Promise 版
    3. 15.3 stream/promises – ストリームの完了待ち
  16. 16. Top-level await と ESM
    1. 16.1 設定ファイルでの利用
    2. 16.2 動的 import との組み合わせ
    3. 16.3 注意点 – 連鎖モジュールロードの遅延
  17. 17. Promise vs Observable – 違いと使い分け
    1. 17.1 値の数と完了の概念
    2. 17.2 キャンセル可能性
    3. 17.3 相互変換
  18. 18. ブラウザ vs Node.js – 環境差を吸収する
    1. 18.1 環境判定
    2. 18.2 共通する Promise 系 API
    3. 18.3 環境差のあるもの
  19. 19. 実践 – 20 のミニパターン集
    1. 19.1 ポーリング(条件を満たすまで Promise で待つ)
    2. 19.2 メモ化(同じ引数の Promise を再利用)
    3. 19.3 重複呼び出しの抑制(in-flight dedup)
    4. 19.4 リソース獲得とクリーンアップ
    5. 19.5 チャンク並列実行
    6. 19.6 Promise のラップでログ追加
    7. 19.7 Promise の進捗通知(自前 progress)
    8. 19.8 ステート機械の状態を Promise で待つ
    9. 19.9 並列のうち最初の N 件だけ採用
    10. 19.10 PromiseState の確認(unwrap ヘルパ)
    11. 19.11 段階的タイムアウト(緩→厳)
    12. 19.12 順次依存を持つチェーン(reduce)
    13. 19.13 stream → Promise(全部読み切る)
    14. 19.14 Promise を Async Iterator に変換
    15. 19.15 タイマーキューの自前実装(setTimeout を 1 つに集約)
    16. 19.16 ジェネレータと Promise(co パターン)
    17. 19.17 排他制御(Mutex / Semaphore)
    18. 19.18 ジッタ付きリトライ(thundering herd 回避)
    19. 19.19 オブザーバ + Promise の AsyncIterator 化
    20. 19.20 Web Worker との Promise ベース通信
  20. 20. アンチパターンとよくある落とし穴
    1. 20.1 Promise constructor アンチパターン
    2. 20.2 forEach での await
    3. 20.3 then チェーンのネスト
    4. 20.4 then の中で例外を握りつぶす
  21. 21. まとめ – Promise を使いこなすための原則

1. Promise とは何か – 状態機械としての理解

Promise を「非同期処理の結果を表すオブジェクト」と説明する記事は多いですが、より正確には「3 状態 1 方向の有限状態機械」として理解するのが本質に近いです。

1.1 Promise の 3 状態と不可逆な遷移

Promise は pending(未確定)からスタートし、fulfilled(成功確定)または rejected(失敗確定)に必ず一度だけ遷移します。一度確定した状態は二度と変わらず、後から resolve / reject を呼んでも無視されます。

// 状態遷移の最小例
const p = new Promise((resolve, reject) => {
  resolve("成功A");
  resolve("成功B");          // 無視される
  reject(new Error("失敗")); // 無視される
});

p.then((v) => console.log(v)); // "成功A" のみ表示

この不可逆性こそが、コールバックよりも Promise が優れている本質的な理由です。コールバックは何度でも呼べてしまうため、「複数回 resolve」「resolve 後の reject」といったバグの温床になります。

1.2 Promise.resolve / Promise.reject – 値からの生成

すでに確定している値から Promise を作るには Promise.resolve / Promise.reject を使います。テストのモックや、同期/非同期を統一的に扱うラッパーで頻出します。

// 即座に fulfilled な Promise
const ok = Promise.resolve(42);
ok.then((v) => console.log(v)); // 42

// 即座に rejected な Promise
const ng = Promise.reject(new Error("即失敗"));
ng.catch((e) => console.error(e.message)); // "即失敗"

// 同期値も非同期値も統一的に扱うラッパー
function ensurePromise(value) {
  return Promise.resolve(value); // すでに Promise ならそのまま返る
}

注意点: Promise.resolve(promise) に Promise を渡すと、ラップせずそのまま返します。Promise.resolve(thenable) に thenable(.then を持つオブジェクト)を渡すと、それを Promise に変換して返します。これが Promise の互換性を支える仕様です。

// thenable を Promise 化
const thenable = {
  then(resolve, reject) {
    setTimeout(() => resolve("thenable から!"), 100);
  },
};

Promise.resolve(thenable).then((v) => console.log(v)); // "thenable から!"

1.3 new Promise(executor) – executor 関数の正しい書き方

new Promise(executor)executor同期的に即実行される関数で、その中で resolve / reject を呼ぶことで状態を確定させます。よくある落とし穴は「executor の中で throw した例外は自動的に reject される」という挙動です。

// executor は即座に実行される
const p = new Promise((resolve, reject) => {
  console.log("executor 内: 即実行");
  setTimeout(() => resolve("非同期結果"), 1000);
});

console.log("Promise 生成後");
// 出力順:
// "executor 内: 即実行"
// "Promise 生成後"
// (1秒後) "非同期結果"

// executor 内で throw すると自動的に rejected
const p2 = new Promise((resolve, reject) => {
  throw new Error("同期エラー");
});
p2.catch((e) => console.error(e.message)); // "同期エラー"

2. then / catch / finally – チェーンの本質

Promise のチェーンは「メソッドを繋いでいる」のではなく、「毎回新しい Promise を返している」のが本質です。これを理解すると、エラー伝播も値変換も自然に書けるようになります。

2.1 then は新しい Promise を返す

.then(onFulfilled, onRejected)必ず新しい Promise を返すメソッドです。コールバック内で return した値は、その新しい Promise の解決値になります。

const p1 = Promise.resolve(10);
const p2 = p1.then((v) => v * 2);    // 新しい Promise<number>
const p3 = p2.then((v) => v + 1);    // さらに新しい Promise<number>

p1.then((v) => console.log("p1:", v)); // 10
p2.then((v) => console.log("p2:", v)); // 20
p3.then((v) => console.log("p3:", v)); // 21

// p1, p2, p3 はそれぞれ独立した Promise
console.log(p1 === p2); // false

2.2 then の中で Promise を return すると平坦化される

then のコールバックが Promise を返した場合、その Promise が解決されるまで次の then は待機します。これが Promise<Promise<T>> にならず Promise<T> になる「平坦化」の仕組みです。

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

function fetchPosts(userId) {
  return fetch(`/api/posts?user=${userId}`).then((res) => res.json());
}

// then の中で Promise を return すると、平坦に繋がる
fetchUser(1)
  .then((user) => fetchPosts(user.id))  // Promise<Post[]> を返す
  .then((posts) => console.log(posts)); // Post[] が直接渡る

2.3 catch は then(undefined, onRejected) の糖衣構文

.catch(handler) は内部的には .then(undefined, handler) と等価です。ただし可読性のため .catch() を使うのが推奨されます。

// この 2 つは等価
Promise.reject(new Error("失敗"))
  .then(undefined, (e) => console.error("v1:", e.message));

Promise.reject(new Error("失敗"))
  .catch((e) => console.error("v2:", e.message));

// .then(onFul, onRej) と .then(onFul).catch(onRej) は微妙に違う
Promise.resolve("成功")
  .then(
    (v) => { throw new Error("then 内で失敗"); },
    (e) => console.error("ここでは捕捉されない")
  )
  .catch((e) => console.error("こっちで捕捉:", e.message));

2.4 finally は値を変更しない

.finally(callback) は成否に関わらず実行されますが、コールバックの戻り値は次の then に渡されません(元の値がそのまま伝播)。ローディング状態の解除など、副作用処理に使います。

let loading = true;

Promise.resolve("データ")
  .finally(() => {
    loading = false;
    return "この値は無視される"; // 影響なし
  })
  .then((v) => console.log(v)); // "データ"(finally の戻り値ではない)

// finally 内で throw した場合のみ、その例外で reject される
Promise.resolve("成功")
  .finally(() => { throw new Error("finally 内エラー"); })
  .catch((e) => console.error(e.message)); // "finally 内エラー"

3. エラー伝播 – チェーンの中での例外の流れ

Promise の最大の利点はエラー伝播が自動で行われることです。チェーンのどこで失敗しても、最下流の catch 1 つで全てキャッチできます。

3.1 エラーは catch まで素通りする

rejected な Promise は、onRejected ハンドラを持つ then または catch に出会うまで、チェーンを素通りします。

Promise.resolve(1)
  .then((v) => v + 1)
  .then((v) => { throw new Error("ここで失敗"); })
  .then((v) => v * 2)                  // ← スキップされる
  .then((v) => console.log("ここも", v)) // ← スキップされる
  .catch((e) => console.error("捕捉:", e.message)); // "ここで失敗"

3.2 catch 後にチェーンを再開できる

catch ハンドラ内で値を return すると、その値でチェーンが復活します。エラー時のフォールバック値に便利です。

function fetchWithFallback(url, fallback) {
  return fetch(url)
    .then((res) => res.json())
    .catch((err) => {
      console.warn("fetch 失敗、フォールバック使用:", err.message);
      return fallback; // チェーン復活
    });
}

fetchWithFallback("/api/config", { theme: "light" })
  .then((config) => console.log("使う設定:", config));

3.3 catch 内で再 throw する

catch 内で throw すると、新たな rejected Promise を生成します。エラー変換のパターンとしてよく使われます。

// 低レベルエラーをドメインエラーに変換
class UserNotFoundError extends Error {
  constructor(id) {
    super(`User ${id} not found`);
    this.name = "UserNotFoundError";
  }
}

function findUser(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => {
      if (res.status === 404) throw new UserNotFoundError(id);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    })
    .catch((err) => {
      // ネットワークエラーもドメインエラー化
      if (err.name === "TypeError") {
        throw new Error("ネットワーク接続失敗");
      }
      throw err; // それ以外は再 throw
    });
}

4. async/await との関係 – 内部実装の理解

async/await はPromise の糖衣構文です。両者は等価変換できるため、内部実装を理解すると Promise でも async/await でも自由に行き来できるようになります。

4.1 await は then の同期的書き方

await pp.then(value => { ...残りのコード... }) と等価です。await 以降のコードは .then の中身として変換されます。

// async/await 版
async function v1() {
  const a = await Promise.resolve(1);
  const b = await Promise.resolve(2);
  return a + b;
}

// Promise 版(等価)
function v2() {
  return Promise.resolve(1).then((a) => {
    return Promise.resolve(2).then((b) => {
      return a + b;
    });
  });
}

v1().then((v) => console.log("v1:", v)); // 3
v2().then((v) => console.log("v2:", v)); // 3

4.2 ライブラリは Promise で書く方が薄い

ライブラリ実装では async/await を使うと余計な await が増えるため、Promise を直接返した方がパフォーマンスが良いケースがあります。

// async 版: 余計な await
async function loadJsonAsync(url) {
  const res = await fetch(url);
  return await res.json(); // この await はマイクロタスクを 1 つ消費
}

// Promise 版: そのまま return
function loadJsonPromise(url) {
  return fetch(url).then((res) => res.json());
}

// 厳密には async 版は Promise を 2 回ラップ&ピールするため、
// 1 マイクロタスク分のオーバーヘッドがある

5. Promise.all – 並列実行の基本

Promise.all(iterable)全ての Promise が成功したら成功1 つでも失敗したら即座に失敗(短絡評価)するという挙動を持ちます。

5.1 配列の Promise を一括待ち

const urls = ["/api/a", "/api/b", "/api/c"];

Promise.all(urls.map((url) => fetch(url).then((r) => r.json())))
  .then(([a, b, c]) => {
    console.log({ a, b, c });
  })
  .catch((err) => {
    // どれか 1 つでも失敗するとここに来る
    console.error("並列取得失敗:", err);
  });

5.2 オブジェクト形式で並列取得(Promise.all + Object.entries)

結果を名前付きで受け取りたい場合は、Object.entries と組み合わせるか、独自ヘルパを書くのが定石です。

// オブジェクト形式の並列取得ヘルパ
function allObject(obj) {
  const entries = Object.entries(obj);
  return Promise.all(entries.map(([k, p]) => p.then((v) => [k, v])))
    .then((pairs) => Object.fromEntries(pairs));
}

// 使用例
allObject({
  user: fetch("/api/me").then((r) => r.json()),
  posts: fetch("/api/posts").then((r) => r.json()),
  config: fetch("/api/config").then((r) => r.json()),
}).then(({ user, posts, config }) => {
  console.log(user, posts, config);
});

5.3 短絡評価の罠 – 他の Promise はキャンセルされない

Promise.all は 1 つでも失敗すると即座に reject されますが、残りの Promise は背後で走り続けます。これは Promise 自体にキャンセル機能がないためで、AbortController を併用して明示的に止める必要があります。

const ctrl = new AbortController();
const { signal } = ctrl;

Promise.all([
  fetch("/api/a", { signal }),
  fetch("/api/b", { signal }),
  fetch("/api/c", { signal }),
])
  .then((results) => console.log(results))
  .catch((err) => {
    console.error("いずれか失敗:", err);
    ctrl.abort(); // 残りを止める
  });

6. Promise.race / allSettled / any – 並列の使い分け

Promise.all 以外の3 つの並列メソッドを正しく使い分けることで、現実のユースケースに即した非同期処理が書けるようになります。

6.1 Promise.race – 最速の結果を取る

Promise.race は最初に確定した Promiseの結果を返します。成功/失敗どちらでも、最速のものが採用されます。タイムアウト実装の定番です。

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// 使用例: 3 秒以内に応答がなければ失敗
withTimeout(fetch("/api/slow"), 3000)
  .then((res) => console.log("間に合った:", res.status))
  .catch((err) => console.error(err.message));

6.2 Promise.allSettled – 全件の結果を集める

Promise.allSettled失敗しても落ちず、全 Promise の結果を { status: "fulfilled" | "rejected", value/reason } の配列で返します。「失敗してもいいから全部試す」用途に最適です。

const urls = ["/api/a", "/api/b", "/api/c"];

Promise.allSettled(urls.map((url) => fetch(url).then((r) => r.json())))
  .then((results) => {
    const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
    const ng = results.filter((r) => r.status === "rejected").map((r) => r.reason);
    console.log(`成功 ${ok.length} 件, 失敗 ${ng.length} 件`);
    return ok;
  })
  .then((data) => console.log("使えるデータ:", data));

6.3 Promise.any – 最初の成功を取る

Promise.any最初に成功した結果を返します。全て失敗した場合は AggregateError が投げられます。「冗長化されたサーバーから一番早く返ってきた成功を採用」のようなパターンに使います。

const mirrors = [
  "https://cdn1.example.com/data.json",
  "https://cdn2.example.com/data.json",
  "https://cdn3.example.com/data.json",
];

Promise.any(mirrors.map((url) => fetch(url).then((r) => r.json())))
  .then((data) => console.log("最速ミラーから取得:", data))
  .catch((err) => {
    // AggregateError
    console.error("全ミラー失敗:", err.errors);
  });

6.4 4 メソッド比較表

4 つの並列メソッドの違いを表で整理します。

// | メソッド         | 成功条件         | 失敗条件         | 戻り値             |
// |------------------|------------------|------------------|--------------------|
// | Promise.all      | 全て成功         | 1 つでも失敗     | 値の配列           |
// | Promise.allSettled | 常に成功(throwしない) | -    | 結果オブジェクト配列 |
// | Promise.race     | 最初の確定が成功 | 最初の確定が失敗 | 単一の値           |
// | Promise.any      | 1 つでも成功     | 全て失敗         | 単一の値           |

7. Promise.withResolvers – ES2024 の新顔

ES2024 で標準化された Promise.withResolvers() は、Promise と resolve/reject を別々に取り出せる静的メソッドです。コールバック地獄から脱出する強力なツールです。

7.1 基本: Deferred パターンの公式実装

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

// どこか別の場所で resolve/reject を呼べる
setTimeout(() => resolve("3 秒後の値"), 3000);

promise.then((v) => console.log(v)); // "3 秒後の値"

7.2 イベントを Promise 化する

1 回しか発火しないイベントを Promise として扱うのに最適です。

function waitForEvent(target, eventName) {
  const { promise, resolve } = Promise.withResolvers();
  target.addEventListener(eventName, resolve, { once: true });
  return promise;
}

// 使用例: クリックを待つ
const button = document.querySelector("#start");
waitForEvent(button, "click").then((event) => {
  console.log("クリックされた!", event.target);
});

7.3 旧来の自前 Deferred 実装と比較

ES2024 以前は自前で書く必要がありました。挙動は同じです。

// 旧来: 自前 Deferred
function deferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// ES2024: 標準
const d1 = deferred();
const d2 = Promise.withResolvers();

// 形は同じ
console.log(Object.keys(d1)); // ["promise", "resolve", "reject"]
console.log(Object.keys(d2)); // ["promise", "resolve", "reject"]

8. 非同期 API の Promise 化

古い API はコールバックスタイルですが、new Promise を使えば簡単に Promise 化できます。

8.1 setTimeout を Promise 化(sleep 関数)

// 最頻出パターン
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// 使用例
sleep(1000).then(() => console.log("1 秒経過"));

// async/await でも自然に使える
(async () => {
  console.log("開始");
  await sleep(500);
  console.log("0.5 秒後");
  await sleep(500);
  console.log("さらに 0.5 秒後");
})();

8.2 FileReader を Promise 化

function readFileAsText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsText(file);
  });
}

// 使用例
const input = document.querySelector("input[type=file]");
input.addEventListener("change", async (e) => {
  const file = e.target.files[0];
  const text = await readFileAsText(file);
  console.log("ファイル内容:", text.slice(0, 100));
});

8.3 Node.js コールバック API の Promise 化(util.promisify)

Node.js には標準で promisify ヘルパがあります。古い fs.readFile などを Promise 化できます。

import { promisify } from "node:util";
import fs from "node:fs";

const readFile = promisify(fs.readFile);

const text = await readFile("./data.txt", "utf-8");
console.log(text);

// 注: 現代の Node.js は最初から fs/promises を提供している
import { readFile as readFilePromise } from "node:fs/promises";
const text2 = await readFilePromise("./data.txt", "utf-8");

8.4 自作 promisify を書く

error-first コールバックを Promise 化する関数は10 行で書けます。理解のために自前実装を見ておきましょう。

// error-first コールバックを Promise 化
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

// 使用例
const asyncFn = (input, cb) => setTimeout(() => cb(null, input * 2), 100);
const promisifiedFn = promisify(asyncFn);
promisifiedFn(21).then((v) => console.log(v)); // 42

9. リトライ・タイムアウト・並列数制限

実務で必須となる3 大パターンを Promise で実装します。これらはほとんどのライブラリ(ky, axios-retry, p-limit など)の内部実装と同じ考え方です。

9.1 リトライ実装(指数バックオフ)

function retry(fn, { retries = 3, baseDelay = 100, factor = 2 } = {}) {
  return new Promise((resolve, reject) => {
    let attempt = 0;
    const tryOnce = () => {
      Promise.resolve()
        .then(fn)
        .then(resolve)
        .catch((err) => {
          if (attempt >= retries) {
            return reject(err);
          }
          const delay = baseDelay * Math.pow(factor, attempt);
          console.warn(`試行 ${attempt + 1} 失敗、${delay}ms 後にリトライ`);
          attempt++;
          setTimeout(tryOnce, delay);
        });
    };
    tryOnce();
  });
}

// 使用例
retry(() => fetch("/api/flaky").then((r) => {
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}), { retries: 5, baseDelay: 200 })
  .then((data) => console.log("取得成功:", data))
  .catch((err) => console.error("全リトライ失敗:", err));

9.2 タイムアウト実装(AbortController 連携)

単に Promise.race するだけでは元の処理が裏で動き続けます。AbortController を組み合わせるのが本物のタイムアウトです。

function fetchWithTimeout(url, { timeout = 5000, ...opts } = {}) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeout);

  return fetch(url, { ...opts, signal: ctrl.signal })
    .finally(() => clearTimeout(timer));
}

// 使用例
fetchWithTimeout("/api/slow", { timeout: 3000 })
  .then((r) => r.json())
  .then((d) => console.log(d))
  .catch((err) => {
    if (err.name === "AbortError") console.error("タイムアウト");
    else console.error("その他の失敗:", err);
  });

9.3 並列数制限(p-limit 風の自前実装)

1000 件の API 呼び出しを並列で投げると相手サーバーがダウンします。並列数を制限するキューを Promise だけで書けます。

function pLimit(concurrency) {
  let active = 0;
  const queue = [];

  const next = () => {
    if (active >= concurrency || queue.length === 0) return;
    active++;
    const { fn, resolve, reject } = queue.shift();
    Promise.resolve()
      .then(fn)
      .then(resolve, reject)
      .finally(() => {
        active--;
        next();
      });
  };

  return (fn) => new Promise((resolve, reject) => {
    queue.push({ fn, resolve, reject });
    next();
  });
}

// 使用例: 同時 3 並列まで
const limit = pLimit(3);
const urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`);

const tasks = urls.map((url) => limit(() => fetch(url).then((r) => r.json())));

Promise.all(tasks).then((results) => {
  console.log(`${results.length} 件完了`);
});

10. キャンセル – AbortController と Promise の連携

Promise 自体にはキャンセル機能がありません。実行を中断したい場合は AbortController と組み合わせるのが ECMAScript 仕様の作法です。

10.1 AbortController の基本

const ctrl = new AbortController();
const { signal } = ctrl;

fetch("/api/data", { signal })
  .then((r) => r.json())
  .then((d) => console.log(d))
  .catch((err) => {
    if (err.name === "AbortError") {
      console.log("キャンセルされた");
    } else {
      console.error("その他:", err);
    }
  });

// 1 秒後にキャンセル
setTimeout(() => ctrl.abort(), 1000);

10.2 任意の Promise を AbortSignal でキャンセルできるようにする

function cancelableSleep(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
    const timer = setTimeout(resolve, ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(new DOMException("Aborted", "AbortError"));
    }, { once: true });
  });
}

// 使用例
const ctrl = new AbortController();
cancelableSleep(5000, ctrl.signal)
  .then(() => console.log("5 秒経過"))
  .catch((err) => console.error(err.name)); // "AbortError"

setTimeout(() => ctrl.abort(), 1000); // 1 秒でキャンセル

10.3 AbortSignal.any() で複数 signal を合成(ES2024)

ES2024 で標準化された AbortSignal.any([s1, s2]) は、どれか 1 つでも abort されたら abort される合成 signal を作ります。タイムアウトとユーザーキャンセルを同時に扱えます。

const userCtrl = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000); // ES2022

// 両方の signal を合成
const combined = AbortSignal.any([userCtrl.signal, timeoutSignal]);

fetch("/api/data", { signal: combined })
  .then((r) => r.json())
  .then((d) => console.log(d))
  .catch((err) => console.error("中断:", err));

// ユーザーが手動キャンセル
document.querySelector("#cancel").addEventListener("click", () => {
  userCtrl.abort();
});

11. マイクロタスク – Promise の実行タイミング

Promise の then コールバックはマイクロタスクとして実行されます。これを理解すると、setTimeout との順序や queueMicrotask の使い分けが見えてきます。

11.1 マクロタスク vs マイクロタスク

マイクロタスク(Promise.then, queueMicrotask, MutationObserver)はマクロタスク(setTimeout, setInterval, I/O)よりも優先的に実行されます。

console.log("1: 同期");

setTimeout(() => console.log("4: マクロタスク (setTimeout)"), 0);

Promise.resolve().then(() => console.log("3: マイクロタスク (Promise)"));

queueMicrotask(() => console.log("3.5: マイクロタスク (queueMicrotask)"));

console.log("2: 同期");

// 出力順:
// 1: 同期
// 2: 同期
// 3: マイクロタスク (Promise)
// 3.5: マイクロタスク (queueMicrotask)
// 4: マクロタスク (setTimeout)

11.2 queueMicrotask の使い所

「同期コードの直後、でも次のレンダリングよりは前」に何かをやりたい時に queueMicrotask を使います。Promise を生成するよりも軽量です。

// 重い: 不要な Promise を生成
Promise.resolve().then(() => doWork());

// 軽量: queueMicrotask
queueMicrotask(() => doWork());

// 実用例: バッチ処理
class Batcher {
  constructor() {
    this.queue = [];
    this.scheduled = false;
  }
  add(item) {
    this.queue.push(item);
    if (!this.scheduled) {
      this.scheduled = true;
      queueMicrotask(() => {
        const items = this.queue.splice(0);
        this.scheduled = false;
        console.log("バッチ実行:", items);
      });
    }
  }
}

const b = new Batcher();
b.add(1); b.add(2); b.add(3); // 1 回のマイクロタスクで処理

11.3 requestAnimationFrame を Promise 化

アニメーション 1 フレームを待つ Promise を作ると、アニメーション処理が読みやすくなります。

const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));

// 使用例: フェードイン
async function fadeIn(el, duration = 500) {
  const start = performance.now();
  while (true) {
    const now = await nextFrame();
    const elapsed = now - start;
    const ratio = Math.min(elapsed / duration, 1);
    el.style.opacity = ratio;
    if (ratio >= 1) break;
  }
}

fadeIn(document.querySelector("#box"));

12. unhandledrejection – 捕捉漏れの監視

catch されない rejected Promise は unhandledrejection イベントを発火します。本番では必ず監視する仕組みを入れるべきです。

12.1 ブラウザでの監視

// 全ての捕捉漏れをロギング
window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
  // Sentry や独自エンドポイントに送信
  fetch("/log/unhandled", {
    method: "POST",
    body: JSON.stringify({
      message: event.reason?.message,
      stack: event.reason?.stack,
      url: location.href,
    }),
  });
  // event.preventDefault(); // ブラウザのデフォルトログを抑制したい場合
});

12.2 Node.js での監視

// Node.js 14+ ではデフォルトでプロセスがクラッシュする
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection:", reason);
  // ロギング後、安全にシャットダウン
  process.exit(1);
});

12.3 catch を忘れがちなパターン

// アンチパターン 1: async 関数の戻り値を捨てる
async function fireAndForget() {
  await fetch("/api/log"); // 失敗したら unhandled
}
fireAndForget(); // catch なし

// 修正版
fireAndForget().catch((err) => console.error(err));

// アンチパターン 2: forEach の中で async
[1, 2, 3].forEach(async (id) => {
  await processItem(id); // forEach は Promise を待たない
});

// 修正版
for (const id of [1, 2, 3]) {
  await processItem(id);
}
// または並列
await Promise.all([1, 2, 3].map(processItem));

13. 自前 Promise キュー – 順次実行の制御

Promise を順次・1 件ずつ処理するキューは、API レート制限や DB トランザクションで頻出します。

13.1 シンプルな順次キュー

class PromiseQueue {
  constructor() {
    this.tail = Promise.resolve();
  }

  add(fn) {
    const result = this.tail.then(fn, fn);
    this.tail = result.catch(() => {}); // エラーをチェーン外に
    return result;
  }
}

// 使用例
const q = new PromiseQueue();
q.add(() => fetch("/api/1").then((r) => r.json()));
q.add(() => fetch("/api/2").then((r) => r.json()));
q.add(() => fetch("/api/3").then((r) => r.json()));
// 3 件が順番に 1 件ずつ実行される

13.2 デバウンス Promise(連打対策)

連打されたら最新の呼び出しだけを処理するパターンです。検索バーのインクリメンタルサーチでよく使います。

function debouncePromise(fn, delay = 300) {
  let timer = null;
  let pendingResolve = null;
  let pendingReject = null;

  return function (...args) {
    if (timer) {
      clearTimeout(timer);
      pendingReject(new Error("Debounced"));
    }
    return new Promise((resolve, reject) => {
      pendingResolve = resolve;
      pendingReject = reject;
      timer = setTimeout(() => {
        Promise.resolve(fn.apply(this, args)).then(resolve, reject);
      }, delay);
    });
  };
}

// 使用例
const search = debouncePromise((q) => fetch(`/search?q=${q}`).then((r) => r.json()));

input.addEventListener("input", async (e) => {
  try {
    const results = await search(e.target.value);
    render(results);
  } catch (err) {
    if (err.message !== "Debounced") console.error(err);
  }
});

14. TypeScript での Promise 型付け

TypeScript では Promise の型推論が強力です。よくある型付けパターンを押さえておきましょう。

14.1 基本的な型注釈

// 明示的に Promise<T> を指定
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// async 関数は戻り値が自動的に Promise でラップされる
async function fetchUserAsync(id: number): Promise<{ name: string }> {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // Promise<{ name: string }> ではなく { name: string } を return
}

// Awaited<T>: Promise の中身を取り出す型
type User = Awaited<ReturnType<typeof fetchUser>>;
// User = { name: string; age: number }

14.2 Promise.all の型推論

TypeScript 4.0 以降、Promise.all はタプル型を正しく推論します。

const p1: Promise<string> = Promise.resolve("hello");
const p2: Promise<number> = Promise.resolve(42);
const p3: Promise<boolean> = Promise.resolve(true);

const results = await Promise.all([p1, p2, p3] as const);
// results: readonly [string, number, boolean]

// 配列のままなら union
const arr = await Promise.all([p1, p2, p3]);
// arr: (string | number | boolean)[]

14.3 Result 型でエラーを型レベルで扱う

Rust 風の Result<T, E> を Promise と組み合わせると、エラーを型として表現できます。

type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E> = Ok<T> | Err<E>;

async function safeFetch<T>(url: string): Promise<Result<T, Error>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
    return { ok: true, value: await res.json() };
  } catch (e) {
    return { ok: false, error: e as Error };
  }
}

// 使用側
const r = await safeFetch<{ name: string }>("/api/me");
if (r.ok) {
  console.log(r.value.name); // 型安全に value にアクセスできる
} else {
  console.error(r.error.message); // 型安全に error にアクセスできる
}

15. Node.js での Promise 活用

Node.js では多くの API がPromise 版を持っています。古いコールバック版から移行するメリットは大きいです。

15.1 fs/promises – ファイルシステム

import { readFile, writeFile, readdir } from "node:fs/promises";

// 並列読み込み
const files = await readdir("./data");
const contents = await Promise.all(
  files.map((f) => readFile(`./data/${f}`, "utf-8"))
);

// 並列書き込み
await Promise.all([
  writeFile("./out/a.txt", "A"),
  writeFile("./out/b.txt", "B"),
  writeFile("./out/c.txt", "C"),
]);

15.2 timers/promises – setTimeout の Promise 版

import { setTimeout as sleep, setInterval } from "node:timers/promises";

// 自前で sleep を書く必要はない
await sleep(1000);
console.log("1 秒経過");

// 非同期イテレータとしての setInterval
for await (const startTime of setInterval(1000, Date.now())) {
  console.log("経過:", Date.now() - startTime, "ms");
  if (Date.now() - startTime > 5000) break;
}

15.3 stream/promises – ストリームの完了待ち

import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";

// ストリーム連結を Promise で完了待ち
await pipeline(
  createReadStream("input.txt"),
  createGzip(),
  createWriteStream("output.txt.gz")
);
console.log("圧縮完了");

16. Top-level await と ESM

ES2022 で標準化された Top-level await により、ESM モジュールのトップレベルで await が使えるようになりました。

16.1 設定ファイルでの利用

// config.mjs
const env = await fetch("/api/config").then((r) => r.json());
export const apiBase = env.apiBase;
export const featureFlags = env.flags;

// 利用側
import { apiBase } from "./config.mjs";
// この時点で apiBase は解決済み

16.2 動的 import との組み合わせ

// 環境に応じて読み込むモジュールを変える
const isBrowser = typeof window !== "undefined";
const platform = isBrowser
  ? await import("./platform-browser.mjs")
  : await import("./platform-node.mjs");

export const log = platform.log;

16.3 注意点 – 連鎖モジュールロードの遅延

Top-level await を多用するとモジュールグラフの初期化が直列化するため、起動時間が伸びることがあります。重い処理は遅延ロードに留めましょう。

// 悪い例: 起動時に重い計算
const heavyData = await import("./heavy.mjs").then((m) => m.compute());

// 良い例: 遅延ロード関数として export
export async function getHeavyData() {
  const m = await import("./heavy.mjs");
  return m.compute();
}

17. Promise vs Observable – 違いと使い分け

Promise と RxJS の Observable はよく比較されますが、本質的に違うものです。Promise は1 つの値、Observable は0 個以上の値のストリームを扱います。

17.1 値の数と完了の概念

// Promise: 1 つの値 (or エラー)
const promise = fetch("/api/data").then((r) => r.json());
promise.then((v) => console.log("唯一の結果:", v));

// Observable: 0 個以上の値 + complete or error
import { interval, take } from "rxjs";
const obs = interval(1000).pipe(take(5));
obs.subscribe({
  next: (v) => console.log("値:", v),  // 5 回呼ばれる
  complete: () => console.log("完了"),
});

17.2 キャンセル可能性

// Promise: キャンセル不可(AbortController を別途使う)
const p = fetch("/api"); // この時点で実行開始

// Observable: subscribe するまで実行されず、unsubscribe でキャンセル可能
import { Observable } from "rxjs";
const obs = new Observable((subscriber) => {
  console.log("購読されたら実行");
  const timer = setTimeout(() => subscriber.next("hello"), 1000);
  return () => clearTimeout(timer); // unsubscribe 時に呼ばれる
});

const sub = obs.subscribe(console.log);
sub.unsubscribe(); // キャンセル

17.3 相互変換

import { from, firstValueFrom } from "rxjs";

// Promise → Observable
const obs$ = from(fetch("/api").then((r) => r.json()));

// Observable → Promise(最初の値だけ)
const value = await firstValueFrom(obs$);

18. ブラウザ vs Node.js – 環境差を吸収する

Promise 仕様自体は両環境で共通ですが、関連 API に差があります。クロス環境ライブラリを書く際の落とし穴を整理します。

18.1 環境判定

const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
const isNode = typeof process !== "undefined" && process.versions?.node;
const isDeno = typeof Deno !== "undefined";
const isBun = typeof Bun !== "undefined";

console.log({ isBrowser, isNode, isDeno, isBun });

18.2 共通する Promise 系 API

// 全環境で使える
Promise.resolve(1);
Promise.reject(new Error("x"));
Promise.all([]);
Promise.race([]);
Promise.allSettled([]);
Promise.any([]);
Promise.withResolvers(); // Node.js 22+, モダンブラウザ
queueMicrotask(() => {});
fetch("..."); // Node.js 18+ で標準
AbortController;
AbortSignal.timeout(1000);
AbortSignal.any([]);

18.3 環境差のあるもの

// ブラウザ専用: requestAnimationFrame, MutationObserver
// Node 専用: process.nextTick, fs/promises, child_process

// nextTick vs queueMicrotask の違い (Node.js)
// nextTick は queueMicrotask よりさらに優先される
process.nextTick(() => console.log("1: nextTick"));
queueMicrotask(() => console.log("2: microtask"));
Promise.resolve().then(() => console.log("3: promise then"));
setImmediate(() => console.log("5: setImmediate"));
setTimeout(() => console.log("4: setTimeout"), 0);

19. 実践 – 20 のミニパターン集

ここまでの知識を活かせる現場で頻出する 20 パターンを一気に紹介します。コピペで動く形にしてあります。

19.1 ポーリング(条件を満たすまで Promise で待つ)

function waitUntil(check, { interval = 100, timeout = 10000 } = {}) {
  const start = Date.now();
  return new Promise((resolve, reject) => {
    const tick = () => {
      if (check()) return resolve();
      if (Date.now() - start > timeout) return reject(new Error("Timeout"));
      setTimeout(tick, interval);
    };
    tick();
  });
}

// 使用例: DOM 要素が出現するまで待つ
await waitUntil(() => document.querySelector("#dynamic-element"));

19.2 メモ化(同じ引数の Promise を再利用)

function memoizePromise(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, Promise.resolve().then(() => fn.apply(this, args)));
    }
    return cache.get(key);
  };
}

// 使用例
const fetchUser = memoizePromise((id) =>
  fetch(`/api/users/${id}`).then((r) => r.json())
);
await fetchUser(1); // ネットワーク呼び出し
await fetchUser(1); // キャッシュから返る

19.3 重複呼び出しの抑制(in-flight dedup)

// 同じキーで進行中の Promise があれば共有
function dedup(fn) {
  const inflight = new Map();
  return function (key, ...args) {
    if (inflight.has(key)) return inflight.get(key);
    const p = Promise.resolve()
      .then(() => fn.apply(this, [key, ...args]))
      .finally(() => inflight.delete(key));
    inflight.set(key, p);
    return p;
  };
}

const fetchData = dedup((id) => fetch(`/api/${id}`).then((r) => r.json()));
const [a, b] = await Promise.all([fetchData("x"), fetchData("x")]);
// ネットワーク呼び出しは 1 回だけ

19.4 リソース獲得とクリーンアップ

// using 構文(Stage 4, ES2026 想定)以前の手書きパターン
async function withResource(acquire, use) {
  const resource = await acquire();
  try {
    return await use(resource);
  } finally {
    if (typeof resource.close === "function") await resource.close();
  }
}

// 使用例
await withResource(
  () => openDatabase("mydb"),
  async (db) => {
    return await db.query("SELECT * FROM users");
  }
);

19.5 チャンク並列実行

// 1000 件を 50 件ずつ並列で処理
async function chunked(items, fn, size = 50) {
  const results = [];
  for (let i = 0; i < items.length; i += size) {
    const chunk = items.slice(i, i + size);
    const batch = await Promise.all(chunk.map(fn));
    results.push(...batch);
  }
  return results;
}

const ids = Array.from({ length: 1000 }, (_, i) => i);
const data = await chunked(ids, (id) =>
  fetch(`/api/${id}`).then((r) => r.json()),
  50
);

19.6 Promise のラップでログ追加

function trace(label, promise) {
  const start = performance.now();
  console.log(`[start] ${label}`);
  return promise.then(
    (v) => {
      console.log(`[ok]    ${label} (${(performance.now() - start).toFixed(1)}ms)`);
      return v;
    },
    (e) => {
      console.log(`[ng]    ${label} (${(performance.now() - start).toFixed(1)}ms):`, e.message);
      throw e;
    }
  );
}

// 使用例
const data = await trace("fetch user", fetch("/api/me"));

19.7 Promise の進捗通知(自前 progress)

function trackProgress(promises, onProgress) {
  let done = 0;
  return Promise.all(
    promises.map((p) =>
      Promise.resolve(p).then((v) => {
        done++;
        onProgress(done, promises.length);
        return v;
      })
    )
  );
}

// 使用例
await trackProgress(
  urls.map((u) => fetch(u)),
  (done, total) => console.log(`${done}/${total} 完了`)
);

19.8 ステート機械の状態を Promise で待つ

class StateMachine {
  constructor(initial) {
    this.state = initial;
    this.waiters = new Map(); // state -> [resolve, ...]
  }

  set(newState) {
    this.state = newState;
    const ws = this.waiters.get(newState) || [];
    ws.forEach((resolve) => resolve());
    this.waiters.delete(newState);
  }

  waitFor(state) {
    if (this.state === state) return Promise.resolve();
    const { promise, resolve } = Promise.withResolvers();
    if (!this.waiters.has(state)) this.waiters.set(state, []);
    this.waiters.get(state).push(resolve);
    return promise;
  }
}

// 使用例
const sm = new StateMachine("idle");
sm.waitFor("ready").then(() => console.log("ready になった!"));
setTimeout(() => sm.set("ready"), 2000);

19.9 並列のうち最初の N 件だけ採用

function firstN(promises, n) {
  return new Promise((resolve, reject) => {
    const results = [];
    let errors = 0;
    promises.forEach((p) => {
      Promise.resolve(p).then(
        (v) => {
          if (results.length < n) {
            results.push(v);
            if (results.length === n) resolve(results);
          }
        },
        () => {
          errors++;
          if (errors > promises.length - n) {
            reject(new Error("十分な数の成功を得られない"));
          }
        }
      );
    });
  });
}

// 使用例: 5 つのソースから 3 件取れれば OK
const data = await firstN(sources.map((u) => fetch(u)), 3);

19.10 PromiseState の確認(unwrap ヘルパ)

// Promise が settled かどうかチェックする裏技
async function getState(promise) {
  const sentinel = Symbol("pending");
  const result = await Promise.race([
    promise.then((v) => ({ status: "fulfilled", value: v })),
    Promise.resolve(sentinel),
  ]);
  if (result === sentinel) return { status: "pending" };
  return result;
}

// 使用例
const p = new Promise((r) => setTimeout(() => r("done"), 1000));
console.log(await getState(p)); // { status: "pending" }
await new Promise((r) => setTimeout(r, 1500));
console.log(await getState(p)); // { status: "fulfilled", value: "done" }

19.11 段階的タイムアウト(緩→厳)

// 1秒で警告、3秒で諦める
async function tieredTimeout(promise, { warn = 1000, fail = 3000 }) {
  const warnTimer = setTimeout(() => console.warn("遅いです..."), warn);
  try {
    return await Promise.race([
      promise,
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), fail)
      ),
    ]);
  } finally {
    clearTimeout(warnTimer);
  }
}

await tieredTimeout(fetch("/api/slow"), { warn: 500, fail: 2000 });

19.12 順次依存を持つチェーン(reduce)

// 前の結果を次の入力にする
const steps = [
  (x) => fetch(`/api/step1?in=${x}`).then((r) => r.json()),
  (x) => fetch(`/api/step2?in=${x}`).then((r) => r.json()),
  (x) => fetch(`/api/step3?in=${x}`).then((r) => r.json()),
];

const result = await steps.reduce(
  (prev, step) => prev.then(step),
  Promise.resolve("initial")
);

19.13 stream → Promise(全部読み切る)

async function streamToText(stream) {
  const reader = stream.getReader();
  const decoder = new TextDecoder();
  let result = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }
  result += decoder.decode();
  return result;
}

// 使用例
const res = await fetch("/api/large");
const text = await streamToText(res.body);

19.14 Promise を Async Iterator に変換

async function* asyncIterFromPromises(promises) {
  for (const p of promises) {
    yield await p;
  }
}

// 完了順にイテレートしたい場合
async function* asAvailable(promises) {
  const pending = new Set(
    promises.map((p, i) => p.then((v) => ({ i, v })))
  );
  while (pending.size > 0) {
    const next = await Promise.race(pending);
    pending.delete(next);
    yield next.v;
  }
}

// 使用例
for await (const v of asAvailable(urls.map((u) => fetch(u)))) {
  console.log("到着:", v);
}

19.15 タイマーキューの自前実装(setTimeout を 1 つに集約)

class TimerQueue {
  constructor() {
    this.timers = [];
    this.next = null;
  }

  add(ms, fn) {
    const at = Date.now() + ms;
    this.timers.push({ at, fn });
    this.timers.sort((a, b) => a.at - b.at);
    this.schedule();
  }

  schedule() {
    if (this.next) clearTimeout(this.next);
    if (this.timers.length === 0) return;
    const delay = Math.max(0, this.timers[0].at - Date.now());
    this.next = setTimeout(() => {
      const now = Date.now();
      while (this.timers[0] && this.timers[0].at <= now) {
        this.timers.shift().fn();
      }
      this.next = null;
      this.schedule();
    }, delay);
  }
}

const q = new TimerQueue();
q.add(300, () => console.log("300ms"));
q.add(100, () => console.log("100ms"));
q.add(500, () => console.log("500ms"));

19.16 ジェネレータと Promise(co パターン)

// async/await 登場以前は、ジェネレータで非同期処理を書いていた
function co(generatorFn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      const gen = generatorFn.apply(this, args);
      function step(method, arg) {
        let res;
        try {
          res = gen[method](arg);
        } catch (e) {
          return reject(e);
        }
        if (res.done) return resolve(res.value);
        Promise.resolve(res.value).then(
          (v) => step("next", v),
          (e) => step("throw", e)
        );
      }
      step("next");
    });
  };
}

// 使用例: async 関数と同等の挙動
const main = co(function* () {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
});

main().then((v) => console.log(v)); // 3

19.17 排他制御(Mutex / Semaphore)

// 1 つの処理を同時に 1 つだけ実行する Mutex
class Mutex {
  constructor() {
    this.queue = Promise.resolve();
  }

  lock(fn) {
    const next = this.queue.then(() => fn());
    this.queue = next.catch(() => {});
    return next;
  }
}

const m = new Mutex();
const r1 = m.lock(async () => {
  await sleep(100);
  return "first";
});
const r2 = m.lock(async () => "second"); // r1 完了後に実行
console.log(await Promise.all([r1, r2])); // ["first", "second"]

19.18 ジッタ付きリトライ(thundering herd 回避)

// 完全な指数バックオフだと、複数クライアントが同時にリトライしてサーバーが落ちる
// ランダムジッタを足すのが定石
async function retryWithJitter(fn, { retries = 5, base = 200, max = 10000 } = {}) {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === retries) throw err;
      const delay = Math.min(base * Math.pow(2, i), max);
      const jitter = Math.random() * delay; // 0〜delay のランダム値
      await new Promise((r) => setTimeout(r, jitter));
    }
  }
}

19.19 オブザーバ + Promise の AsyncIterator 化

// EventTarget から AsyncIterator を作る
async function* eventsToAsyncIterator(target, eventName, { signal } = {}) {
  const queue = [];
  let resolveNext = null;

  const handler = (e) => {
    if (resolveNext) {
      resolveNext(e);
      resolveNext = null;
    } else {
      queue.push(e);
    }
  };
  target.addEventListener(eventName, handler);
  signal?.addEventListener("abort", () => {
    target.removeEventListener(eventName, handler);
    if (resolveNext) resolveNext({ done: true });
  });

  try {
    while (!signal?.aborted) {
      if (queue.length > 0) {
        yield queue.shift();
      } else {
        const e = await new Promise((r) => (resolveNext = r));
        if (e?.done) return;
        yield e;
      }
    }
  } finally {
    target.removeEventListener(eventName, handler);
  }
}

// 使用例
const ctrl = new AbortController();
for await (const event of eventsToAsyncIterator(button, "click", { signal: ctrl.signal })) {
  console.log("クリック:", event);
}

19.20 Web Worker との Promise ベース通信

// worker.js
self.addEventListener("message", async (e) => {
  const { id, type, payload } = e.data;
  try {
    let result;
    if (type === "heavy") result = doHeavyWork(payload);
    self.postMessage({ id, ok: true, result });
  } catch (err) {
    self.postMessage({ id, ok: false, error: err.message });
  }
});

// main.js
class WorkerClient {
  constructor(url) {
    this.worker = new Worker(url);
    this.callbacks = new Map();
    this.id = 0;
    this.worker.addEventListener("message", (e) => {
      const { id, ok, result, error } = e.data;
      const cb = this.callbacks.get(id);
      this.callbacks.delete(id);
      ok ? cb.resolve(result) : cb.reject(new Error(error));
    });
  }

  call(type, payload) {
    const id = ++this.id;
    const { promise, resolve, reject } = Promise.withResolvers();
    this.callbacks.set(id, { resolve, reject });
    this.worker.postMessage({ id, type, payload });
    return promise;
  }
}

// 使用例
const w = new WorkerClient("/worker.js");
const result = await w.call("heavy", { input: 1000 });

20. アンチパターンとよくある落とし穴

Promise を使う上で避けるべき書き方を整理します。レビューでも頻出する論点です。

20.1 Promise constructor アンチパターン

既存の Promise を new Promise で包み直すのは無意味です。

// 悪い例: 二重ラップ
function getData() {
  return new Promise((resolve, reject) => {
    fetch("/api/data")
      .then(resolve)
      .catch(reject);
  });
}

// 良い例: そのまま return
function getData() {
  return fetch("/api/data");
}

20.2 forEach での await

// 悪い例: forEach は await を待たない
[1, 2, 3].forEach(async (id) => {
  await processItem(id);
});
console.log("完了"); // すぐに表示される(処理は終わっていない)

// 良い例: for...of で直列、Promise.all で並列
for (const id of [1, 2, 3]) {
  await processItem(id); // 直列
}
await Promise.all([1, 2, 3].map(processItem)); // 並列

20.3 then チェーンのネスト

// 悪い例: ネストしすぎ
fetchUser(1).then((user) => {
  fetchPosts(user.id).then((posts) => {
    fetchComments(posts[0].id).then((comments) => {
      console.log(comments);
    });
  });
});

// 良い例: 平坦化
fetchUser(1)
  .then((user) => fetchPosts(user.id))
  .then((posts) => fetchComments(posts[0].id))
  .then((comments) => console.log(comments));

20.4 then の中で例外を握りつぶす

// 悪い例: エラーが見えなくなる
fetch("/api").then((r) => {
  try {
    return JSON.parse(r); // 失敗しても catch されない
  } catch {
    return null;
  }
});

// 良い例: エラーは throw、フォールバックは catch
fetch("/api")
  .then((r) => r.json())
  .catch((err) => {
    console.error("パース失敗:", err);
    return null;
  });

21. まとめ – Promise を使いこなすための原則

Promise は非同期処理のプリミティブです。async/await は便利な糖衣構文ですが、ライブラリ実装・並列制御・キャンセル・低レイヤ最適化を行う場面では、Promise そのものを理解し操る力が必要になります。

本記事で扱った要点を箇条書きで再確認します。

  • 状態機械として理解する: Promise は pending → fulfilled/rejected の一方向遷移
  • then は新しい Promise を返す: チェーンは「合成」であって「連結」ではない
  • 4 つの並列メソッドを使い分ける: all / race / allSettled / any それぞれの短絡条件を覚える
  • Promise.withResolvers (ES2024): Deferred パターンの公式実装を活用する
  • キャンセルは AbortController: Promise 自体にキャンセル機能はない。AbortSignal.any で合成可能
  • マイクロタスクと優先度: Promise.then / queueMicrotask は setTimeout より優先
  • unhandledrejection の監視: 本番では必ずロギングを仕込む
  • 並列数制限・リトライ・タイムアウト: 30 行程度で自前実装可能。ライブラリの内部を理解する
  • TypeScript の Awaited<T>: Promise の中身を型として取り出せる
  • async/await との等価変換: 両者は同じ仕組み。状況に応じて使い分ける

Promise を「いつ使うか」ではなく「常に使っている前提でどう操るか」という観点に切り替えると、より高度な非同期処理が書けるようになります。よりハイレベルな書き味を求める場合は async/await 完全実践ガイド を、JavaScript 全般の品質向上を求める場合は JavaScript ベストプラクティス 10 選 を合わせて参照してください。

コメント

タイトルとURLをコピーしました