第7章:Promise合成の罠 Promise.all など 🧨🧵
この章は「複数の非同期処理をまとめる」ときに起きがちな事故を、ぜんぶ先回りして潰す回だよ〜!😆✨
Promise.all は便利だけど、便利すぎて地雷も多いんだよね…💣💥
この章のゴール 🎯✨
読み終わったら、こんな状態になってるのが目標だよ🫶
Promise.all / allSettled / any / raceの「何が起きるか」を説明できる🙂Promise.allで 「どれが失敗したのか分からない」問題を解決できる🔎- 「失敗しても全結果が欲しい」「1個成功でOK」など、目的で使い分けできる🧠✨
- 原因を失わない合成パターンを、手癖として持てる🧵🎁
7-1 合成ってなに? 並列のまとめ方 🌈⚡
非同期をまとめたい場面って、だいたいこのどれか👇
- ✅ 全部成功したらOK:例)ユーザー情報・注文履歴・おすすめを同時に取る
- ✅ 全部の結果が欲しい:例)10個の画像URLのうち、失敗も含めて結果一覧が欲しい
- ✅ どれか1個成功でOK:例)複数ミラーサーバーから「最初に成功したやつ」を採用
- ✅ 一番早いものを採用:例)タイムアウトと本処理を競争させる
ここで使うのが Promise の合成メソッドたちだよ🧵✨
特に Promise.all はよく使われるけど、罠も多い…!😱
7-2 Promise.all の基本 便利だけど性格が強い😎⚡
Promise.all はこういう性格だよ👇
- 全部成功したら成功(配列で値が返る)
- 1つでも失敗したら即失敗(最初の失敗理由で reject) (MDNウェブドキュメント)
- ただし!他の処理は止まらずに走り続ける(結果は
allの戻り値からは拾えない) (MDNウェブドキュメント)
この「止まらず走り続ける」が、合成事故の温床〜〜〜!🧨🧨🧨
7-3 罠その1 1つ落ちた瞬間に全体が落ちる でも裏で他は動く😱⚡
![罠その1 1つ落ちた瞬間に全体が落ちる[(./picture/err_model_ts_study_007_domino_chain.png)
たとえばこんな感じ👇
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
async function okFast() {
await sleep(50);
return "OK_FAST";
}
async function failMid() {
await sleep(100);
throw new Error("FAIL_MID");
}
async function okSlow() {
await sleep(200);
return "OK_SLOW";
}
async function main() {
try {
const results = await Promise.all([okFast(), failMid(), okSlow()]);
console.log("ALL OK:", results);
} catch (e) {
console.log("ALL FAILED:", e);
}
}
main();
これ、failMid() が落ちた瞬間に Promise.all は失敗するよね💥
でも okSlow() は裏で走り続けるよ! (MDNウェブドキュメント)
何が困るの?😵💫
- 「全体失敗」として画面を出したのに、裏で成功して状態が進む😱
- DB更新や課金処理みたいな 副作用が混ざると、最悪「半分だけ反映」になる💀
ここでの合言葉🪄
Promise.all は “キャンセル機構” じゃない
「全体失敗にしたい」なら、失敗時に他を止める設計も必要になるよ(後で race + AbortController も出すね)🧯
7-4 罠その2 どれが失敗したのか分からない🙈💥
Promise.all が返してくれるのは、基本「最初の失敗理由」だけ! (MDNウェブドキュメント)
だからこうなる👇
- A も B も C もやってて
- B が失敗した
- でも catch で見えるのは「Error: なんか失敗」だけ
- どの処理が失敗したの?🤯 ってなる
解決策 失敗にラベルを貼る🏷️✨
「どの処理の失敗か」を分かるように、失敗時に情報を足すよ🎁
type TaggedError = {
tag: string;
cause: unknown;
};
function tag<T>(tag: string, p: Promise<T>): Promise<T> {
return p.catch((cause) => {
const err: TaggedError = { tag, cause };
throw err;
});
}
async function main() {
try {
const [user, orders, recs] = await Promise.all([
tag("fetchUser", fetchUser()),
tag("fetchOrders", fetchOrders()),
tag("fetchRecs", fetchRecs()),
]);
console.log({ user, orders, recs });
} catch (e) {
// ここで「どれが失敗したか」が分かる✨
if (typeof e === "object" && e !== null && "tag" in e) {
const te = e as TaggedError;
console.log("FAILED TAG:", te.tag);
console.log("CAUSE:", te.cause);
} else {
console.log("FAILED UNKNOWN:", e);
}
}
}
これだけで デバッグ難易度が激下がりするよ〜〜!😆✨
7-5 罠その3 失敗が1個しか見えない 残りの失敗が消える😶🌫️💥
Promise.all は「最初の失敗理由」だけで reject するよね。 (MDNウェブドキュメント)
だから、もし他にも失敗があったとしても、
- all の catch からは見えない
- 場合によっては「ログに残らない」「監視に引っかからない」みたいな事故が起きる😱
こういうときに使うのが次👇
7-6 Promise.allSettled 成功も失敗も全部ちょうだい📦✨
Promise.allSettled は性格が真逆で、全員が終わるまで待ってから結果をくれる!
「成功・失敗を含む一覧」が欲しいときに超強い💪 (MDNウェブドキュメント)
const results = await Promise.allSettled([fetchUser(), fetchOrders(), fetchRecs()]);
for (const r of results) {
if (r.status === "fulfilled") {
console.log("OK:", r.value);
} else {
console.log("NG:", r.reason);
}
}
これが刺さる場面🎯
- 10件中 2件失敗しても「残り8件は表示したい」📺✨
- バッチ処理で「成功と失敗の一覧レポート」が欲しい🧾✅
- エラーの棚卸しをしたい(まさにエラーモデリング向き!)📚🧠
7-7 Promise.any どれか1個成功でOK 救世主スタイル🦸♀️✨
Promise.any はこういう性格👇
- 最初に成功したやつで fulfill する
- 全部失敗したら
AggregateErrorで reject(errorsに理由配列) (MDNウェブドキュメント) Promise.raceと違って「最初の成功」を狙う(失敗は無視して進む) (MDNウェブドキュメント)
例:ミラーAPIを3つ叩いて、一番最初に成功したレスポンスを採用🌐✨
async function fetchFromMirror(urls: string[]) {
try {
return await Promise.any(urls.map((u) => fetch(u).then((r) => r.json())));
} catch (e) {
// 全部失敗したら AggregateError
if (e instanceof AggregateError) {
console.log("ALL FAILED 😭");
console.log("reasons:", e.errors); // 失敗理由の配列
}
throw e;
}
}
AggregateErrorは複数エラーをまとめるための標準エラーだよ 🧺✨ (MDNウェブドキュメント)
7-8 Promise.race 一番早く決着したものを採用🏁⚡
Promise.race は「最初に settle したもの」で決着するよ。
成功でも失敗でも、先に決まった方が勝ち🏁 (MDNウェブドキュメント)
よくある使い方 タイムアウト⏳💥
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout ${ms}ms`)), ms)
);
}
async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
return Promise.race([p, timeout(ms)]);
}
でも!ここも罠😱
race でタイムアウトになっても、本体の処理は裏で走り続けることがあるよ…(all と同じ系統の罠)🧨
対策 AbortController で止める🛑🧯
fetch なら中止できる!✨
async function fetchJsonWithTimeout(url: string, ms: number) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(id);
}
}
「合成」って、止められるかどうかもセットで考えると事故りにくいよ🧠✨
7-9 目的別 使い分け早見表🗺️✨
-
✅ 全部成功したらOK・失敗なら全体失敗 →
Promise.all(MDNウェブドキュメント)- ただし「他が走り続ける」前提で副作用に注意⚠️ (MDNウェブドキュメント)
-
✅ 成功も失敗も一覧が欲しい →
Promise.allSettled(MDNウェブドキュメント) -
✅ どれか1個成功でOK →
Promise.any(全部失敗はAggregateError) (MDNウェブドキュメント) -
✅ 成功でも失敗でも最初に決着したもの →
Promise.race(MDNウェブドキュメント)
7-10 ミニ演習📝✨ どれが失敗したか追える Promise.all を作ろう🔎🏷️
お題🎀
3つの非同期関数があるとして👇
loadProfile()loadOrders()loadRecs()
これを同時実行しつつ、失敗したら「どれが失敗したか」が分かるようにしてね🙂✨
ヒント🧠
- 失敗にラベルを付けて throw し直す🏷️🎁
解答例✅
type TaggedError = { tag: string; cause: unknown };
function tag<T>(tag: string, p: Promise<T>): Promise<T> {
return p.catch((cause) => {
throw { tag, cause } satisfies TaggedError;
});
}
async function loadAll() {
return await Promise.all([
tag("profile", loadProfile()),
tag("orders", loadOrders()),
tag("recs", loadRecs()),
]);
}
async function main() {
try {
const [profile, orders, recs] = await loadAll();
console.log({ profile, orders, recs });
} catch (e) {
if (typeof e === "object" && e !== null && "tag" in e) {
const te = e as TaggedError;
console.log("FAILED:", te.tag, te.cause);
} else {
console.log("FAILED:", e);
}
}
}
7-11 AI活用🤖✨ この章で使うと強いプロンプト例
コピペで使ってOKだよ〜!💌😆
- 「TypeScriptで Promise.all の失敗にタグを付けたい。tag関数の実装案を3つ。メリデメも。」🤖
- 「Promise.allSettled の結果を Result っぽい union に変換する関数を書いて。型も丁寧に。」🧠
- 「Promise.race + timeout を作ったけど、裏で処理が残る問題を避ける設計を提案して。fetch前提。」🧯
- 「この並列処理の失敗ケースを洗い出して、UI表示方針もセットで提案して。」🎀
AIに出させた案は、そのまま採用じゃなくて “なんでそうなる?”を自分の言葉で説明できるかをゴールにすると、めちゃ伸びるよ📈✨
7-12 まとめ この章の超大事ポイント3つ💎✨
Promise.allは 1個落ちたら即失敗、でも 他は走り続ける (MDNウェブドキュメント)- 「どれが失敗?」問題は タグ付けで解決🏷️✨
- 「結果全部ほしい」「1個成功でOK」「タイムアウトしたい」は
allSettled / any / raceを目的で使い分け🗺️ (MDNウェブドキュメント)
次の章では、この「複数非同期をどこで受け止める?」みたいな話につながっていくよ🚪🧭✨