JavaScript | 非同期処理:Promise 基礎 – catch の基本

JavaScript JavaScript
スポンサーリンク

まず catch を一言でいうと

catch は、
「Promise で起きたエラー(失敗)を、最後にまとめて受け止める場所」
です。

then が「成功したときの続きを書く場所」だとしたら、
catch「どこかで失敗したときの“落とし穴”として待ち構える場所」 です。

ここが重要です。
catch を使うことで、
「非同期処理のどこでエラーが起きても、最後に 1 箇所でエラー処理をまとめて書ける」
ようになります。これはコールバック時代の if (err) { ... } だらけの世界と比べて、とても大きな進歩です。


catch の基本的な使い方

いちばんシンプルな形

形はとても簡単です。

promise.catch(onRejected);
JavaScript

promiserejected(失敗)になったとき、
その「エラー情報」を引数として onRejected が呼ばれます。

たとえば:

const p = new Promise((resolve, reject) => {
  reject(new Error("何かに失敗しました"));
});

p.catch((error) => {
  console.log("エラー:", error.message); // エラー: 何かに失敗しました
});
JavaScript

ここで起きていることは、

Promise 作成直後:pending
すぐ reject(...) を呼ぶ → 状態が rejected に変わる
catch に登録されたコールバックが、「失敗理由」を受け取って実行される

という流れです。

then と組み合わせた基本パターン

実務でよく見るのは、こういう形です。

doSomethingAsync()
  .then((result) => {
    console.log("成功:", result);
  })
  .catch((error) => {
    console.error("失敗:", error);
  });
JavaScript

doSomethingAsync() が返す Promise が、

成功したら → then が呼ばれる
失敗したら → catch が呼ばれる

という、非常に素直な分担になっています。

ここが重要です。
「成功は then」「失敗は catch」という役割分担で考えると、Promise のコードがかなり読みやすくなる
と覚えておいてください。


catch が「どこまでのエラー」を拾うのか

チェーン全体のエラーを一箇所で拾える

Promise の強みは、
.then を何段もつなげても、
最後の catch 一つで、途中のどこで発生したエラーもまとめて拾える ことです。

例を見てみます。

doStep1()          // Promise を返すとする
  .then((r1) => {
    console.log("step1:", r1);
    return doStep2(r1);   // これも Promise
  })
  .then((r2) => {
    console.log("step2:", r2);
    return doStep3(r2);   // これも Promise
  })
  .then((r3) => {
    console.log("step3:", r3);
  })
  .catch((error) => {
    console.error("どこかで失敗:", error);
  });
JavaScript

ここでは、

doStep1 が失敗して reject された場合
doStep2 が失敗した場合
doStep3 が失敗した場合
あるいはそれぞれの then の中で throw された場合

そのどれであっても、
最後の catch に流れてきます。

これは、同期コードでいうと

try {
  const r1 = doStep1Sync();
  const r2 = doStep2Sync(r1);
  const r3 = doStep3Sync(r2);
} catch (e) {
  console.error("どこかで失敗:", e);
}
JavaScript

にかなり近いイメージです。

途中の then で throw した場合も catch に届く

次のコードを見てください。

Promise.resolve(10)
  .then((v) => {
    console.log("v:", v);
    throw new Error("ここでエラー");
  })
  .then((v2) => {
    console.log("これは実行されない");
  })
  .catch((err) => {
    console.log("catch で拾った:", err.message);
  });
JavaScript

流れはこうなります。

Promise.resolve(10) → 10 で成功済みの Promise
1 つ目の then で 10 を受け取り、ログを出したあと throw
その throw が、「内部的に Promise の reject」として扱われる
→ 次の then はスキップされ、catch に飛ぶ

つまり、

reject を明示的に呼ばなくても、then の中で起きた例外は自動的に catch に流れる

という動きになっています。

ここが重要です。
catch は「reject されたエラー」だけでなく、「then の中で投げられた例外」もまとめて受け止める。
これによって、「エラーがどこで起きたか」をいちいち意識せずに、末尾で一括処理できる
ようになっています。


then(onFulfilled, onRejected) より catch を使う方が良い理由

then の第2引数にも「エラー処理」は書ける

実は catch を使わなくても、
then の第2引数に失敗時の処理を書けます。

promise.then(
  (value) => {
    console.log("成功:", value);
  },
  (error) => {
    console.log("失敗:", error);
  }
);
JavaScript

これでも動きます。
しかし、これはあまり推奨されません。

中途半端な場所で「エラーを止めてしまいやすい」

問題は、then(onFulfilled, onRejected) を使うと、

「その then の中だけでエラーを止めてしまい、その後ろのチェーンにエラーが伝わらない」

という状況を作りやすいことです。

たとえば:

doStep1()
  .then(
    (r1) => {
      return doStep2(r1);
    },
    (err) => {
      console.log("step1 のエラー:", err);
      // ここで何も return しないと、
      // 次の then は「成功扱い」で動き始めてしまうことがある
    }
  )
  .then((r2) => {
    console.log("step2:", r2);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

途中の onRejected でエラーを「処理したつもり」で終わらせてしまうと、
後続の catch に伝わらず、
「一部だけ挙動が違う」状態になりがちです。

そこで、

成功はすべて then
失敗は最後に catch

という形にしておくと、
「途中でエラーを飲み込んでしまう」ミスが減ります。

実務的に一番シンプルなパターン

なので、基本形としてはこれを覚えておくのをおすすめします。

somePromise
  .then(...)
  .then(...)
  .then(...)
  .catch((err) => {
    // どこで起きてもここに来るようにする
  });
JavaScript

ここが重要です。
「エラーはとりあえず最後の catch に全部流す」という設計にしておくと、
Promise チェーン全体の挙動がシンプルで読みやすくなる。


catch も Promise を返す(続きの処理も書ける)

catch のあとにさらに then をつなげられる

catchthen と同じく、「新しい Promise」を返します。

doSomething()
  .catch((err) => {
    console.error("エラー:", err);
    return "デフォルト値";  // エラー時の代わりの値
  })
  .then((value) => {
    console.log("その後の処理:", value);
  });
JavaScript

この例では、

doSomething が成功した場合
→ catch は実行されず、結果が次の then にそのまま渡る

doSomething が失敗した場合
→ catch が呼ばれ、「デフォルト値」を返す
→ 新しい Promise がその「デフォルト値」で成功したものとして扱われる
→ 次の then に value として “デフォルト値” が渡る

という動きになります。

つまり、catch

エラーをログなどで処理するだけ
+ 必要なら「代わりの値」を返して、処理を続ける

という役割も果たせます。

ここが重要です。
catch も then と同じく、「値(または Promise)を返すことで、エラーから“復帰”して処理を続けることができる」。
必ずしも「そこで終わり」ではない
、という点を覚えておくと便利です。


「同期の try/catch」との対応関係をイメージする

同期世界:try/catch

同期コードでは、こう書きます。

try {
  const r1 = step1();
  const r2 = step2(r1);
  const r3 = step3(r2);
  console.log("成功:", r3);
} catch (e) {
  console.error("どこかでエラー:", e);
}
JavaScript

この中で例外が起きると、
catch に飛んで一括処理できます。

非同期世界:then/catch

Promise では、同じ流れをこう書けます。

step1Async()
  .then((r1) => step2Async(r1))
  .then((r2) => step3Async(r2))
  .then((r3) => {
    console.log("成功:", r3);
  })
  .catch((e) => {
    console.error("どこかでエラー:", e);
  });
JavaScript

見比べると、

try の中 → then のチェーン
catch の中 → Promise の catch

という対応になっているのが分かると思います。

ここが重要です。
Promise の catch は、「非同期版 try/catch の catch」にあたる存在 として考えると、役割がかなりスッキリ理解できます。


初心者向け「catch の押さえどころ」まとめ

ここまでのポイントを、必要最低限に絞ってまとめます。

catch は、Promise が rejected(失敗)になったときに実行する処理を登録するメソッド。

catch は、その前の then チェーンのどこかで起きたエラー(rejectthrow)をまとめて受け止める。

then の第2引数でもエラー処理は書けるが、基本は「成功は then」「失敗は最後の catch」という形にするとシンプルで安全。

catch も新しい Promise を返すので、エラー時に代わりの値を返したりして、その後も .then で処理を続けることができる。

同期の try { ... } catch (e) { ... }catch と、役割的に対応していると考えると理解しやすい。

最後にもう一度だけ強調すると、
ここが重要です。
catch は「エラーを集めて受け止める最後の受け皿」であり、
「エラー処理をバラバラに書いていた世界から、1 箇所にまとめて書ける世界に連れていってくれる道具」
です。

練習としては、

わざと reject する Promise を作る
then を 2〜3 個つないでから catch をつける
どこで失敗させても catch に来るか試す

この 3 ステップをやってみると、
「catch がチェーン全体の“最後の守護神”になっている感覚」が、自分の中でかなりハッキリしてくるはずです。

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