JavaScript | 非同期処理:エラー処理・例外設計 – catch の共通化

JavaScript JavaScript
スポンサーリンク

「catch の共通化」を一言でいうと

「catch の共通化」は、
「あちこちにバラバラに書いているエラー処理を、意味のある“ひとつの場所”にまとめること」 です。

同じような catch (err) { console.error(err); } が画面ごと・関数ごとに散らばっていると、
どこで何が起きているのか分かりづらくなります。

逆に、
「通信エラーはここでまとめて処理する」
「認証エラーはここでログイン画面に飛ばす」
といった“共通の窓口”を作ると、コードの見通しが一気によくなります。

ここが重要です。
catch の共通化は、
単に「関数にまとめる」だけではなく、
「どの種類のエラーを、どのレイヤーで扱うか」を設計すること です。
その設計があると、非同期処理のエラーが“流れ”として見えるようになります。


なぜ catch がバラバラだとつらくなるのか

ありがちな「とりあえず catch」だらけのコード

例えば、こんなコードを想像してみてください。

async function loadUsers() {
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    renderUsers(data);
  } catch (err) {
    console.error("ユーザー取得エラー:", err);
    alert("ユーザーの取得に失敗しました");
  }
}

async function loadProfile() {
  try {
    const res = await fetch("/api/profile");
    const data = await res.json();
    renderProfile(data);
  } catch (err) {
    console.error("プロフィール取得エラー:", err);
    alert("プロフィールの取得に失敗しました");
  }
}

async function loadSettings() {
  try {
    const res = await fetch("/api/settings");
    const data = await res.json();
    renderSettings(data);
  } catch (err) {
    console.error("設定取得エラー:", err);
    alert("設定の取得に失敗しました");
  }
}
JavaScript

どの関数も、ほぼ同じような catch を持っています。

ログの出し方も、ユーザーへのメッセージも、
全部コピペで増えていきます。

この状態だと、次のような問題が出てきます。

エラーメッセージを変えたいとき、全部の関数を探して直さないといけない。
ログのフォーマットを変えたいときも、全部書き換え。
認証エラーだけログイン画面に飛ばしたい、となったときに、全部の catch に条件分岐を足すことになる。

つまり、
「エラー処理が重複していて、変更に弱い」 状態です。

共通化すると何が嬉しいか

もし、エラー処理を共通化できていれば、

「通信エラーのときはこのメッセージ」
「認証エラーのときはログイン画面へ」
「それ以外は汎用エラー表示」

といったルールを一箇所に書くだけで済みます。

新しい API を叩く関数を追加しても、
「成功時の処理」だけ書けばよくて、
エラー時の処理は共通の仕組みに任せられます。

ここが重要です。
catch の共通化は、
「エラー処理を DRY(Don’t Repeat Yourself)にする」ことでもあり、
「アプリ全体のエラー体験を一貫させる」ための土台
でもあります。


一番シンプルな共通化:共通関数にまとめる

共通のエラーハンドラ関数を作る

まずは、素直に「エラーを受け取って処理する関数」を作るところから始めましょう。

function handleError(err) {
  console.error(err);

  alert("エラーが発生しました。時間をおいて再度お試しください。");
}
JavaScript

これを使うと、先ほどのコードはこう書き換えられます。

async function loadUsers() {
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    renderUsers(data);
  } catch (err) {
    handleError(err);
  }
}

async function loadProfile() {
  try {
    const res = await fetch("/api/profile");
    const data = await res.json();
    renderProfile(data);
  } catch (err) {
    handleError(err);
  }
}
JavaScript

これだけでも、
「エラー表示の文言を変えたい」「ログの出し方を変えたい」といったときに、
handleError だけ直せば済むようになります。

エラーの種類で分岐する

もう少し踏み込んで、
エラーの種類によって処理を変えることもできます。

function handleError(err) {
  console.error(err);

  if (err.isNetworkError) {
    alert("ネットワークエラーが発生しました。接続を確認してください。");
    return;
  }

  if (err.isUnauthorized) {
    alert("ログインが必要です。");
    redirectToLogin();
    return;
  }

  alert("エラーが発生しました。時間をおいて再度お試しください。");
}
JavaScript

このようにしておけば、
各画面のコードは「エラーを投げる or そのまま catch に流す」だけでよくなり、
「どう見せるか」は共通関数に任せられます。

ここが重要です。
共通化の第一歩は、「エラーを受け取って、ログ+ユーザー向けメッセージを出す関数」を一つ作ること。
そこに“エラーの種類ごとの分岐”を集約していくと、アプリ全体のエラー挙動が揃っていきます。


fetch 自体をラップして catch を共通化する

共通の API 呼び出し関数を作る

もう一段進めて、
「fetch を直接呼ばず、共通のラッパー関数を通す」
という設計もよく使われます。

例えば、こんな関数を用意します。

async function callApi(url, options = {}) {
  try {
    const response = await fetch(url, {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        ...(options.headers || {}),
      },
      ...options,
    });

    if (!response.ok) {
      const error = new Error("HTTP エラー: " + response.status);
      error.status = response.status;
      throw error;
    }

    return await response.json();
  } catch (err) {
    handleError(err);
    throw err;
  }
}
JavaScript

この callApi は、

fetch の呼び出し
ステータスコードのチェック
JSON への変換
エラー時の共通処理(handleError)

を全部まとめています。

各画面のコードは、こうなります。

async function loadUsers() {
  try {
    const data = await callApi("/api/users");
    renderUsers(data);
  } catch (err) {
    // ここでは何もしない(共通処理に任せる)か、
    // 画面固有の処理だけ書く
  }
}
JavaScript

「共通 catch の中で throw し直す」意味

callApi の中で handleError(err); を呼んだあと、
throw err; でエラーを投げ直しているのがポイントです。

これをすることで、

共通処理(ログ・アラートなど)は callApi の中で行う
それでも「上のレイヤーにエラーを伝えたい」ときは、再度 throw して伝播させる

という二段構えができます。

例えば、画面側で「ローディング表示を消す」などの後処理をしたい場合、
finally を使ってこう書けます。

async function loadUsers() {
  setLoading(true);

  try {
    const data = await callApi("/api/users");
    renderUsers(data);
  } catch (err) {
    // ここでは画面固有の処理だけ(必要なら)
  } finally {
    setLoading(false);
  }
}
JavaScript

ここが重要です。
fetch をラップした共通関数の中で catch し、
そこで「ログ+ユーザー向けメッセージ」を処理しつつ、
必要なら throw し直して上のレイヤーに伝える。
これが、非同期エラー処理の“共通化の王道パターン”の一つです。


Promise チェーンでの catch 共通化

チェーンの最後に一つだけ catch を置く

async / await ではなく、
then / catch の Promise チェーンでも共通化はできます。

例えば、こういう書き方です。

function handlePromiseError(err) {
  console.error("Promise エラー:", err);
  alert("エラーが発生しました");
}

fetch("/api/users")
  .then((res) => {
    if (!res.ok) {
      throw new Error("HTTP エラー: " + res.status);
    }
    return res.json();
  })
  .then((data) => {
    renderUsers(data);
  })
  .catch(handlePromiseError);
JavaScript

ここでは、
チェーンの最後に .catch(handlePromiseError) を一つだけ置いています。

途中の then の中で throw したエラーも、
fetch のネットワークエラーも、
全部この handlePromiseError に流れてきます。

複数の API 呼び出しがある場合も、
同じ handlePromiseError を使い回せます。

fetch("/api/users")
  .then(...)
  .catch(handlePromiseError);

fetch("/api/profile")
  .then(...)
  .catch(handlePromiseError);
JavaScript

共通 catch の中で「エラーの種類」を見分ける

handlePromiseError の中で、
エラーの種類によって処理を変えることもできます。

function handlePromiseError(err) {
  console.error(err);

  if (err.status === 401) {
    alert("ログインが必要です");
    redirectToLogin();
    return;
  }

  alert("エラーが発生しました");
}
JavaScript

ここが重要です。
Promise チェーンでは、「最後の catch を共通関数にする」だけでも、
エラー処理の重複をかなり減らせます。
「どのチェーンも最後は handlePromiseError に流す」というルールを決めると、
エラーの出口が一つに揃います。


「共通化しすぎ」の落とし穴と、ちょうどいいバランス

何でもかんでも共通 catch に押し込むと困ること

共通化は便利ですが、
やりすぎると「どこで何が起きたのか分からない」状態にもなりえます。

例えば、すべてのエラーを一つの handleError で処理して、
そこから先は何も伝えない、という設計にすると、

どの画面で起きたエラーなのか分からない
画面ごとの「この場合だけこうしたい」という要望に対応しづらい

といった問題が出てきます。

「共通化」と「画面固有の処理」のバランスが大事です。

レイヤーごとに役割を分けるイメージ

おすすめは、
レイヤーごとに「どこまで共通化するか」を決めることです。

API 呼び出しレイヤー(callApi など)
ここでは「HTTP エラー」「ネットワークエラー」など技術的なエラーを共通処理。
ログを出したり、認証エラーならログインに飛ばしたり。

画面レイヤー(loadUsers など)
ここでは「この画面としてどう振る舞うか」を決める。
例えば、「ユーザー一覧が取れなかったら空表示にする」「エラー用の UI を出す」など。

このように、
「技術的なエラー処理」は共通化し、
「画面ごとの振る舞い」は各画面に任せる

という分け方をすると、共通化と柔軟性のバランスが取りやすくなります。

ここが重要です。
catch の共通化は、「全部を一箇所に押し込む」ことではありません。
「どのレイヤーで、どの種類のエラーを扱うか」を決めて、
“共通で扱うべきもの” だけをまとめる
ことです。
その線引きができると、エラー処理は一気に設計っぽくなります。


初心者として「catch の共通化」で本当に押さえてほしいこと

同じような catch があちこちにあると、
エラー処理の変更がつらくなり、アプリ全体の挙動もバラバラになる。

まずは「エラーを受け取ってログ+ユーザー向けメッセージを出す共通関数」を一つ作る。
そこにエラーの種類ごとの分岐を集約していく。

fetch を直接使うのではなく、
callApi のような共通ラッパー関数を作り、その中で try / catch して handleError を呼ぶ。
必要ならエラーを throw し直して、上のレイヤーに伝える。

Promise チェーンでは、
チェーンの最後の .catch(...) を共通関数にして、
どのチェーンもそこに流すようにする。

そして何より大事なのは、
「どこまでを共通化し、どこからを画面固有にするか」を意識して決めること。

コードを書くときに、
「このエラーはアプリ全体で同じ扱いにしたいか?」
「それとも、この画面だけの特別な振る舞いが必要か?」
と自分に問いかけてみてください。

その問いを重ねるほど、
catch は「とりあえず書くもの」から、
“エラーの流れをデザインするための道具” に変わっていきます。

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