JavaScript | 非同期処理:エラー処理・例外設計 – 同期例外と非同期例外

JavaScript JavaScript
スポンサーリンク

同期例外と非同期例外を一言でいうと

同期例外と非同期例外の違いは、
「エラーが いつ 投げられて、どこで 捕まえられるか」の違いです。

同期例外は、
「その関数を呼んだ“その瞬間”に起きるエラー」。

非同期例外は、
「関数を呼んだ“あとで”(タイマーが動いた後・通信が終わった後など)に起きるエラー」。

この違いを理解していないと、

「try…catch 書いたのに、エラーが取れない」
「Promise のエラーがコンソールにだけ出てる」

という “よくある混乱” にハマります。

ここが重要です。
JavaScript のエラー処理で一番大事なのは、
「このエラーは“その場”で起きるのか、“あとで”起きるのか」 を見分ける感覚です。
そこが分かると、try…catch / Promise / async/await の役割が一気に整理されます。


同期例外とは何か(その場で起きるエラー)

同期処理の中で起きる例外のイメージ

同期例外は、「今この瞬間に実行しているコードの中で起きるエラー」です。

例えば、次のようなコードです。

function divide(a, b) {
  if (b === 0) {
    throw new Error("0 で割ることはできません");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log("結果:", result);
} catch (err) {
  console.log("捕まえたエラー:", err.message);
}
JavaScript

関数 divide(10, 0) を呼んだ瞬間、
関数の中で throw new Error(...) が実行されます。
これは「同期的に」、つまりその場で発生する例外です。

try ブロック内でその関数を呼んでいるので、
このエラーはすぐに catch に飛び、
「捕まえたエラー: 0 で割ることはできません」と表示されます。

同期例外の特徴

同期例外のポイントを言葉でまとめると、こうなります。

関数呼び出し → すぐ中で throw → その関数を呼んでいる try に飛ぶ。

関数の中から、さらに別の関数を呼んでいても、
「スタックの上に向かって」try…catch を探しに行きます。

function c() {
  throw new Error("Cでエラー");
}

function b() {
  c();
}

function a() {
  b();
}

try {
  a();
} catch (err) {
  console.log("捕まえた:", err.message);
}
JavaScript

ここでは c() の中でエラーが起きますが、
それは b → a と伝わり、最終的に try { a(); }catch に捕まります。

ここが重要です。
同期例外は、「呼び出しの“スタック”の中で、その場で起きて、その場で上に伝わる」タイプのエラーです。
この世界では、try…catch は素直に期待どおりに動きます。


非同期例外とは何か(あとで起きるエラー)

setTimeout の中のエラーは「あとで」起きる

非同期処理でありがちなのが、次のパターンです。

try {
  setTimeout(() => {
    throw new Error("タイマーの中でエラー");
  }, 1000);
} catch (err) {
  console.log("捕まえた:", err.message);
}
JavaScript

一見、「try…catch で囲んでいるから捕まるはず」と思いますよね。
ところが、この catch は呼ばれません。
コンソールには未処理のエラーとして出てしまいます。

なぜかというと、

setTimeout のコールバックが実行されるのは「1秒後」だからです。
そのときには、すでに try {...} catch {...} のスコープは「通り過ぎて」います。

タイムラインで見ると、こんな感じです。

  1. try ブロックが実行される
  2. setTimeout(...) を予約して、try ブロックは何事もなく終わる
  3. 1 秒後、コールバック関数が別のタイミングで実行される
  4. その中で throw が起きるが、もうそこには try…catch は存在しない

このように、「非同期で実行されるコールバックの中で起きるエラー」は、外側の同期的な try…catch では捕まえられません。

Promise の中のエラーも「非同期例外」

Promise も同じです。

try {
  new Promise((resolve, reject) => {
    throw new Error("Promise 内でエラー");
  });
} catch (err) {
  console.log("捕まえた:", err.message);
}
JavaScript

これも catch には入りません。
この throw は「Promise を reject する」という形に変換されるだけで、
外側の try…catch には届きません。

Promise のエラーは、catchthen(..., onRejected) で扱う必要があります。

new Promise((resolve, reject) => {
  throw new Error("Promise 内でエラー");
})
  .then(() => {
    console.log("成功");
  })
  .catch((err) => {
    console.log("Promise の catch で捕まえた:", err.message);
  });
JavaScript

ここが重要です。
非同期例外の本質は、
「エラーが発生するタイミングが、“外側の try…catch が終わったあと” になってしまう」ことです。
だから、非同期の世界には「非同期のためのエラーの受け皿」(Promise の catch や async/await の try…catch)が別に必要になります。


async / await と try…catch の関係(ここが実務の中心)

async 関数の中は「同期的に見える非同期世界」

async / await を使うと、「非同期処理を同期っぽく書ける」とよく言われます。

例えば fetch の例を見てみましょう。

async function loadData() {
  try {
    const response = await fetch("https://example.com/api/data");
    const data = await response.json();
    console.log("成功:", data);
  } catch (err) {
    console.log("捕まえたエラー:", err.message);
  }
}
JavaScript

このコードは、

await fetch(...)await response.json() の中で起きたエラーを、
ちゃんと catch で捕まえることができます。

ここで重要なのは、

await は「Promise の完了(resolve / reject)を、この行で待つ」
→ reject されたとき、それを「ここで throw されたかのように」扱う

という動きをする、ということです。

つまり、

「Promise の reject を、“同期っぽい例外” に翻訳してくれる」

のが await です。

async 関数の外側の try…catchでは捕まらないもの

注意したいのは、「async 関数を呼び出す側」の try…catchです。

async function loadData() {
  const response = await fetch("https://example.com/api/data");
  const data = await response.json();
  return data;
}

try {
  loadData(); // ここで await していない
} catch (err) {
  console.log("捕まえた?", err.message);
}
JavaScript

この catch には、エラーは入りません。

理由は簡単で、loadData() は Promise を返しているだけだからです。
その時点ではまだ中の処理は完了しておらず、
エラーが起きるのは「あとで」(非同期)です。

エラーを捕まえたいなら、呼び出し側でもちゃんと await する必要があります。

try {
  const data = await loadData();
  console.log(data);
} catch (err) {
  console.log("ちゃんと捕まえた:", err.message);
}
JavaScript

ここが重要です。
async / await の世界では、

「async 関数の中の try…catch」
→ その関数の中で await している非同期のエラーを捕まえる場所。

「async 関数を呼び出す側の try…catch」
→ その async 関数を await したときに起きるエラーを捕まえる場所。

という二段構えになっています。
“await していない Promise は、同期の try…catch では捕まらない” という点を、何度も意識してください。


どこで try…catch して、どこで catch() するのか

Promise チェーンでのエラーの流れ

Promise を then / catch で書く場合、
エラー処理は .catch(...) に集まります。

fetch("https://example.com/api/data")
  .then((response) => response.json())
  .then((data) => {
    console.log("成功:", data);
  })
  .catch((err) => {
    console.log("Promise チェーンで捕まえた:", err.message);
  });
JavaScript

途中の then の中で throw したり、Promise.reject したりしても、
すべて最後の catch に流れてきます。

このとき、外側を同期的な try…catch で包んでも、意味はほとんどありません。

try {
  fetch("https://example.com/api/data")
    .then(...);
} catch (err) {
  // ここには来ない
}
JavaScript

非同期例外は、基本「Promise チェーン上の catch で受け止める」か、
「async 関数の中の try…catch で受け止める」かのどちらかです。

設計としてどう分けるか

設計の考え方としては、次のようなイメージを持つと整理しやすくなります。

同期的な処理(データ変換、計算、バリデーションなど)
→ その場の try…catch で扱う(普通の同期例外)。

非同期的な処理(fetch, setTimeout, Promiseベースの処理)
→ async/await の中なら try…catch、
Promise チェーンなら .catch で扱う。

さらに、そのエラーを上のレイヤーに伝えたいときは、
「一旦ここでエラーメッセージを整形して(ログを取りつつ)、改めて throw し直す」
という形もよく使います。

ここが重要です。
エラー処理は、「同期の世界の try…catch」と「非同期の世界の catch / async-try…catch」を意識的に分けること。
どの層で、どの種類のエラーを拾って、どこまで上に伝えるか。
それを設計として決めると、例外設計が“場当たり”ではなく“意図のある”ものになります。


初心者として「同期例外と非同期例外」で本当に押さえてほしいこと

同期例外は、「関数を呼んだ“その場”で起きるエラー」。
普通の try…catch は、この同期例外に対して素直に効く。

非同期例外は、「setTimeout のコールバック」「Promise の中」「fetch など非同期処理の結果」といった、“あとで” 実行される場所で起きるエラー。
外側の同期的な try…catch では捕まえられない。

Promise を使う非同期エラーは、
then / catch なら .catch(...)
async / await なら await を含む try…catch で扱うのが基本。

async 関数を呼ぶ側でエラーを捕まえたいなら、
その関数を「呼ぶ」だけではだめで、必ず await しないといけない。

ここが重要です。
コードを書くとき、エラー処理を考える前に、
「この処理は同期か?非同期か?」
「エラーが起きるのは“今すぐ”か?それとも“あとで”か?」
を自分に問いかけてみてください。
その問いが習慣になると、
「try…catch を書いたのに効かないんだけど?」
という種類の混乱は、かなり減っていきます。

最後に、理解を深める小さな練習をおすすめします。

同期の例として、関数の中で throw するコードを書き、
外側の try…catch で捕まることを確認する。

setTimeout の中で throw するコードを書き、
外側の try…catch では捕まらないことを実際に目で見る。

fetch を Promise チェーン(then / catch)で書いたものと、
async / await + try…catch で書いたものを両方作って、
どこでエラーが捕まっているかを自分の言葉で説明してみる。

「いつ」「どこで」「どうやって」エラーが伝わっていくのか。
そこを追いかけること自体が、非同期と例外設計を理解する一番の近道です。

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