まず「エラー伝播」を一言でいうと
Promise のエラー伝播は、
「どこかの then で失敗しても、その“失敗情報”がチェーンを下へ流れていき、最後の catch まで届く仕組み」
のことです。
コールバック地獄のときは、
毎回 if (err) { ... } と書いて、各場所でエラーを処理する必要がありました。
Promise では、
上でエラーが起きる
→ そのエラーが下に向かって「バケツリレー」される
→ 最後の .catch(...) でまとめて受け止められる
という形にできます。
ここが重要です。
「エラーは、何もしなければ“下流へ流れ続ける”」
これが Promise のエラー伝播の基本ルールです。
同期の try/catch をイメージにしてみる
同期コードでのエラーの流れ
まず、同期の try/catch から考えてみます。
function step1() {
console.log("step1");
throw new Error("step1 でエラー");
}
function main() {
try {
step1(); // ここでエラー
console.log("ここには来ない");
} catch (e) {
console.error("捕まえた:", e.message);
}
}
main();
JavaScriptstep1 の中でエラーが起きると、
step1 から main へエラーが「上に向かって」伝わり、
try/catch の catch で受け止められます。
「エラーが起きた場所」と「エラーを処理する場所」が離れていても、
途中で誰もキャッチしなければ、上へ上へと伝播していく感じです。
Promise は「非同期版 try/catch」として考えると分かりやすい
Promise の then/catch も、本質的には同じです。
then のどこかでエラーが起きる
→ 途中で誰も「ちゃんと処理」しなければ
→ 最後の catch まで伝播する
この「伝播の方向」が、
同期では「呼び出し元へ上向き」
Promise では「チェーンの下流へ下向き」
に違うだけで、考え方はかなり近いです。
ここが重要です。
Promise のエラー伝播は、「非同期版 try/catch のエラーがどこに届くか」として捉えると理解しやすい。
基本のパターン:どこで失敗しても最後の catch に来る
3段チェーンでのエラー伝播
まずは、典型的な 3 段チェーンの例です。
function step1() {
return Promise.resolve("step1 OK");
}
function step2() {
return Promise.reject(new Error("step2 で失敗"));
}
function step3() {
return Promise.resolve("step3 OK");
}
step1()
.then((r1) => {
console.log(r1);
return step2(); // ここで失敗する Promise を返す
})
.then((r2) => {
console.log(r2); // 実行されない
return step3();
})
.then((r3) => {
console.log(r3); // 実行されない
})
.catch((err) => {
console.error("catch:", err.message);
});
JavaScript実行の流れを言葉で追うとこうなります。
step1 が成功し、最初の then の中が実行される
→ step2() を return(これは「失敗する Promise」)
→ step2 が reject する(= 失敗)
→ その時点でチェーン全体が「失敗状態」になる
→ 以降の then はスキップされ、最後の catch に飛ぶ
出力されるのは、
step1 OK
catch: step2 で失敗
だけです。
ここが重要です。
途中のどこかで Promise が reject されると、そのあとの then はスキップされ、次に現れた catch までエラーが伝播する。
これが基本の挙動です。
then の中で throw したエラーも伝播する
reject だけがエラーじゃない
エラー伝播は、reject されたときだけではありません。then の中で throw されたエラーも、同じように伝播します。
Promise.resolve(10)
.then((v) => {
console.log("value:", v);
throw new Error("then の中でエラー");
})
.then((v2) => {
console.log("これは実行されない");
})
.catch((err) => {
console.log("catch:", err.message);
});
JavaScriptここで起きていることは、
最初の Promise が 10 で成功
→ 1つ目の then が 10 を受け取る
→ その中で throw new Error(...)
→ 内部的に Promise が「rejected」とされる
→ 2つ目の then はスキップ
→ catch にエラーが伝播してくる
という流れです。
つまり、
「明示的な reject」
も
「then の中での throw」
も、どちらも同じように「下流の catch へ伝播するエラー」 になります。
ここが重要です。
「エラーが起きる場所」が Promise の中なのか then の中なのかはあまり気にしなくてよい。
どこで起きても、catch までの間に誰もちゃんと処理しなければ、エラーは下まで流れていく。
エラーが「伝播しなくなる」のはどんなときか
catch でエラーを「飲み込む」と伝播が止まる
エラーは、catch によって「処理」すると伝播が止まることがあります。
Promise.reject(new Error("最初のエラー"))
.catch((err) => {
console.log("ここで処理:", err.message);
// 何も return しない or 正常な値を返す
})
.then(() => {
console.log("その後の then");
})
.catch((err) => {
console.log("最後の catch:", err.message);
});
JavaScriptこの場合、
最初に reject される
→ 1つ目の catch にエラーが渡る
→ そこでログを出して終わり(エラーを“消化”した)
→ 何も throw していないので、次は「成功扱い」の Promise として続く
→ 次の then が実行され、「その後の then」と表示
→ 2つ目の catch には何も来ない
というふうになります。
「catch でエラーを受け取って、そこで終わり」にすると、
それ以上そのエラーは下流に流れていきません。
「やっぱりエラーとして伝えたい」なら再スローする
「一部ではログだけ出して、でも最終的には上位にエラーとして伝えたい」場合は、
catch の中で再び throw する、または Promise.reject を返します。
Promise.reject(new Error("最初のエラー"))
.catch((err) => {
console.log("ログだけ出す:", err.message);
throw err; // 再スロー
})
.then(() => {
console.log("ここは実行されない");
})
.catch((err) => {
console.log("最終的な catch:", err.message);
});
JavaScriptこの場合、
1つ目の catch で一度エラーを受ける
→ ログを出したあと、再び throw
→ その throw によって新たに rejected 状態となる
→ その後の then はスキップされ
→ 2つ目の catch で受け止められる
という流れになります。
ここが重要です。
catch は「エラーを止める場所」にも「エラーを整形して次へ渡す場所」にもなりうる。
何も return しない/正常値を返せばエラーは止まり、throw すればエラーは引き続き伝播する。
then(onFulfilled, onRejected) を使うときの落とし穴
第2引数でエラーを処理すると、伝播が分かりにくくなる
then には第2引数としてエラー処理を書くこともできます。
promise.then(
(value) => { /* 成功時 */ },
(error) => { /* 失敗時 */ }
);
JavaScriptしかし、これはエラー伝播の理解を難しくすることが多いです。
例を見てみます。
Promise.reject(new Error("エラー"))
.then(
(v) => {
console.log("成功:", v);
},
(err) => {
console.log("then 内でエラー処理:", err.message);
// ここで何も throw しない
}
)
.then(() => {
console.log("その後の then");
})
.catch((err) => {
console.log("最後の catch:", err.message);
});
JavaScriptこの場合、
エラーは第2引数の onRejected で処理される
→ その then の中で throw しない限り、「その後の then」は成功として続く
→ 最後の catch には何も届かない
という挙動になります。
「え、エラーあったのに最後の catch に来ないの?」と混乱しがちです。
基本は「成功は then」「失敗は catch」に分ける
そのため、Promise を使うときは、
then の第1引数には成功時の処理
失敗時は then の第2引数ではなく、最後の catch で扱う
というスタイルに統一しておくと、エラー伝播が非常に分かりやすくなります。
somePromise
.then((value) => {
// 成功時
})
.then((value2) => {
// さらに続き
})
.catch((err) => {
// どこかでの失敗をまとめて受ける
});
JavaScriptここが重要です。
エラー伝播を素直に理解するためには、「成功は then の第1引数」「失敗は最後の catch」と役割を分けるのがベスト。
finally とエラー伝播(軽く触れる)
finally は「エラーの有無に関係なく通過する」
finally は「成功でも失敗でも必ず実行される」場所でした。
Promise.reject(new Error("エラー"))
.finally(() => {
console.log("後片付け");
})
.catch((err) => {
console.log("catch:", err.message);
});
JavaScriptここでは、
Promise が rejected
→ finally が実行
→ その後、元のエラーはそのまま catch に伝播
という流れです。
元が success なら success のまま、
元が error なら error のまま、
基本的に状態を変えずにそのまま下流へ流します。
ただし、finally の中でエラーを投げると、その新しいエラーが下流に伝播します。
初心者向け「エラー伝播」のまとめ
Promise のエラー伝播を、いちばん大事なポイントだけに絞るとこうなります。
Promise が reject されるか、then の中で throw されると、「エラー」が発生する。
そのエラーは、次に現れる catch までチェーンを「下流へ」伝わっていく。
エラーが伝播している途中の then はスキップされる(実行されない)。
catch の中でエラーを「処理して終わり」にすると、そこで伝播は止まる。
もう一度 throw したり、Promise.reject を return すると、「新しいエラー」としてさらに下流へ伝播する。
then の第2引数でエラーを扱うと伝播が複雑になるので、基本は「成功は then」「失敗は最後の catch」という分担にする。
ここが重要です。
Promise のチェーンを読むときは、「今この時点でエラーは起きているか? そのエラーは catch まで流されているか? 途中で飲み込まれていないか?」を意識して追うこと。
おすすめの練習は、
わざと step2 で reject するチェーン
わざと then の中で throw するチェーン
をそれぞれ書いてみて、
「どこまで then が実行されて、どこから catch に飛ぶのか」
を console.log で確認することです。
一度でもその動きを自分の目で見ておくと、
「エラーは、下に向かって自然に流れていく」「catch が“最後の受け皿”になる」
という感覚がしっかりと腑に落ちてきます。
