JavaScript | 非同期処理:Promise 基礎 – エラー伝播

JavaScript JavaScript
スポンサーリンク

まず「エラー伝播」を一言でいうと

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();
JavaScript

step1 の中でエラーが起きると、

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 が“最後の受け皿”になる」
という感覚がしっかりと腑に落ちてきます。

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