JavaScript | 非同期処理:エラー処理・例外設計 – エラー情報の整形

JavaScript JavaScript
スポンサーリンク

「エラー情報の整形」を一言でいうと

「エラー情報の整形」は、
「バラバラで生々しいエラー情報を、“人間やアプリが扱いやすい形” に整理し直すこと」 です。

生のエラーは、たいていこうです。

・メッセージが英語で長い
・スタックトレースがずらっと出る
・API から返ってきた JSON がそのまま入っている
・ユーザーに見せるには情報が多すぎる or 不親切

そこで、
「ログ用にはこう整える」
「ユーザー向けにはこう要約する」
「アプリ内部ではこういう共通フォーマットにする」
といった“変換”を行うのが「エラー情報の整形」です。

ここが重要です。
エラー情報の整形は、
「エラーを隠す」のではなく、「エラーを意味のある形に翻訳する」作業 です。
非同期処理(fetch / Promise / async/await)では、ここをサボると一気にカオスになります。


なぜ「生のエラー」をそのまま使うとつらいのか

生の Error オブジェクトは“開発者向け”すぎる

例えば、単純なエラーを投げてみます。

try {
  throw new Error("Something bad happened");
} catch (err) {
  console.error(err);
}
JavaScript

コンソールには、メッセージと一緒にスタックトレースがずらっと出ます。
これは開発者にとってはありがたい情報ですが、
ユーザーにそのまま見せるものではありません。

非同期処理になると、さらに情報が増えます。

try {
  const res = await fetch("https://example.com/api/data");
  if (!res.ok) {
    throw new Error("HTTP error " + res.status);
  }
} catch (err) {
  console.error(err);
}
JavaScript

ここで err をそのままユーザーに alert したら、
「HTTP error 500」みたいな、
意味は分かるけど不親切なメッセージになります。

API からのエラーもそのままだと扱いづらい

API がこんな JSON を返してくるとします。

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "メールアドレスの形式が正しくありません",
    "details": {
      "email": "有効なメールアドレスを入力してください"
    }
  }
}

これをそのまま err に突っ込んでしまうと、

・どこに何が入っているのか毎回バラバラ
・画面ごとにパースの仕方が違う
・ログにもユーザーにも同じ情報を出してしまう

という状態になりがちです。

ここが重要です。
「生のエラー情報」は、“素材” にすぎません。
そのまま使うと、ログも UI もバラバラになり、
どこで何が起きているのか追いにくくなります。
だからこそ、一度“整形レイヤー”を通す価値があるのです。


エラー情報を「共通フォーマット」に整形する

共通フォーマットのイメージ

まず、「アプリ内部で扱うエラー情報の形」を決めてしまうと楽になります。

例えば、こんな形を共通フォーマットと決めるとします。

{
  type: "network" | "api" | "validation" | "unknown",
  message: "ユーザーに見せられるメッセージ",
  rawError: Error や元のレスポンスなど生の情報,
  status: HTTP ステータス(あれば),
  code: サーバー側のエラーコード(あれば)
}
JavaScript

この形に「変換」してしまえば、
画面側は「type と message だけ見ればだいたい分かる」状態になります。

整形用の関数を作る

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

function normalizeError(err) {
  // すでに整形済みならそのまま返す
  if (err && err.__normalized) {
    return err;
  }

  const normalized = {
    type: "unknown",
    message: "エラーが発生しました。時間をおいて再度お試しください。",
    rawError: err,
    status: undefined,
    code: undefined,
    __normalized: true,
  };

  // ネットワークエラーっぽいもの
  if (err?.name === "TypeError" && err.message === "Failed to fetch") {
    normalized.type = "network";
    normalized.message = "ネットワークエラーが発生しました。接続を確認してください。";
    return normalized;
  }

  // カスタムエラー(後で出てくる ApiError など)に対応
  if (err?.name === "ApiError") {
    normalized.type = "api";
    normalized.message = err.message;
    normalized.status = err.status;
    normalized.code = err.code;
    return normalized;
  }

  // それ以外は unknown のまま
  return normalized;
}
JavaScript

この normalizeError は、
「生の err を受け取って、アプリ内で扱いやすい形に変換する」役割を持ちます。

ここが重要です。
「normalize(正規化)」する関数を一つ決めておくと、
どんなエラーが来ても、最終的には同じ形で扱えるようになります。
これが“エラー情報の整形”の中核です。


fetch + カスタムエラー + 整形の流れを具体的に見る

まずカスタムエラーで「意味」を付ける

前の会話で出てきたような ApiError を使います。

class ApiError extends Error {
  constructor(message, status, body, code) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
    this.code = code;
  }
}
JavaScript

fetch をラップして ApiError を投げる

共通の API 呼び出し関数を作ります。

async function callApi(url, options = {}) {
  let response;

  try {
    response = await fetch(url, {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        ...(options.headers || {}),
      },
      ...options,
    });
  } catch (err) {
    // ネットワークレベルのエラー(そもそもサーバーに届いていない)
    throw err; // ここではまだ整形しない
  }

  let body = null;
  try {
    body = await response.json();
  } catch (e) {
    // JSON でない場合は body は null のままでもよい
  }

  if (!response.ok) {
    const serverMessage = body?.error?.message;
    const serverCode = body?.error?.code;

    const message =
      serverMessage ||
      `サーバーでエラーが発生しました(${response.status})`;

    throw new ApiError(message, response.status, body, serverCode);
  }

  return body;
}
JavaScript

ここまでで、

・ネットワークエラー → 生の Error
・HTTP エラー → ApiError

という「意味付け」ができました。

呼び出し側で「整形」してから扱う

次に、画面側のコードで normalizeError を使います。

async function loadUsers() {
  try {
    const body = await callApi("/api/users");
    renderUsers(body.data);
  } catch (err) {
    const e = normalizeError(err);

    console.error("ログ用:", e.rawError);

    if (e.type === "network") {
      showErrorMessage(e.message);
    } else if (e.type === "api") {
      showErrorMessage(e.message);
    } else {
      showErrorMessage(e.message);
    }
  }
}
JavaScript

ここでのポイントは、

・ログには e.rawError(生の情報)を出す
・ユーザーには e.message(整形済みのメッセージ)だけを見せる

という役割分担ができていることです。

ここが重要です。
「カスタムエラーで“意味”を付ける」
→ 「normalizeError で“形”を揃える」
→ 「画面側では type と message だけを見て振る舞いを決める」
という三段階の流れができると、非同期エラー処理が一気に整理されます。


ユーザー向けメッセージとログ向け情報を分けて整形する

ユーザーに見せるべき情報はかなり少ない

例えば、サーバーからこんなエラーが返ってきたとします。

{
  "error": {
    "code": "EMAIL_ALREADY_USED",
    "message": "This email is already registered.",
    "debug": "UserId=123, RequestId=abc-xyz"
  }
}

開発者にとっては全部大事ですが、
ユーザーに「This email is already registered.」と英語で出しても親切ではありません。

そこで、整形の段階で「ユーザー向けメッセージ」を決めます。

function toUserMessage(normalizedError) {
  if (normalizedError.type === "network") {
    return "ネットワークエラーが発生しました。接続を確認してください。";
  }

  if (normalizedError.type === "api") {
    if (normalizedError.code === "EMAIL_ALREADY_USED") {
      return "このメールアドレスは既に登録されています。";
    }
    return normalizedError.message; // サーバーからのメッセージをそのまま使う or 汎用文言
  }

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

ログには「生の情報+整形後」を両方残す

ログ用には、もう少しリッチな情報を残します。

function logError(normalizedError) {
  console.group("アプリケーションエラー");
  console.error("type:", normalizedError.type);
  console.error("status:", normalizedError.status);
  console.error("code:", normalizedError.code);
  console.error("message:", normalizedError.message);
  console.error("raw:", normalizedError.rawError);
  console.groupEnd();
}
JavaScript

画面側では、こう使えます。

catch (err) {
  const e = normalizeError(err);
  logError(e);
  const userMessage = toUserMessage(e);
  showErrorMessage(userMessage);
}
JavaScript

ここが重要です。
エラー情報の整形は、「ユーザー向け」と「ログ向け」を分けることでもあります。
ユーザーには短く分かりやすく、
ログには詳細で生々しく。
その橋渡しをするのが“整形レイヤー”です。


非同期処理ならではの整形ポイント

どの段階で失敗したのかをメッセージに反映する

非同期処理では、失敗ポイントがいくつかあります。

・リクエストを送る前(バリデーション)
・リクエスト送信中(ネットワークエラー)
・レスポンス受信後の HTTP エラー
・レスポンス JSON のパースエラー
・その後のアプリ内処理(データ変換など)のエラー

整形の段階で、「どこで失敗したか」をメッセージに反映できます。

例えば、ApiError に「phase(段階)」を持たせることもできます。

class ApiError extends Error {
  constructor(message, status, body, code, phase) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
    this.code = code;
    this.phase = phase; // "request" | "response" | "parse" など
  }
}
JavaScript

これを使えば、ログに

「レスポンス受信後のパースで失敗」
「レスポンスは来たが、ステータスが 500」

といった情報を残せます。

再試行やリカバリの判断材料にする

整形されたエラー情報を使って、
「このエラーは再試行すべきか?」
「ユーザーに再読み込みボタンを出すべきか?」
といった判断もできます。

例えば、

・type が “network” → 再試行候補
・status が 500〜599 → 再試行候補
・code が “VALIDATION_ERROR” → 再試行しても意味がない(入力を直す必要がある)

といったルールを、整形後の情報に対して書けます。

ここが重要です。
非同期処理では、「エラー情報の整形」がそのまま「次のアクションの判断材料」になります。
整形されたエラーが賢ければ賢いほど、アプリのふるまいも賢くできます。


初心者として「エラー情報の整形」で本当に押さえてほしいこと

生の Error や API レスポンスを、そのまま UI やロジックに使うと、
コードもメッセージもバラバラになりやすい。

まずは「アプリ内で扱うエラーの共通フォーマット」を決める。
type, message, status, code, rawError など、
自分のアプリに必要な項目を一つのオブジェクトにまとめる。

カスタムエラー(ApiError など)で「意味」を付け、
normalizeError のような関数で「形」を揃える。
画面側は、その整形済みエラーだけを見て振る舞いを決める。

ユーザー向けメッセージとログ向け情報を分ける。
ユーザーには短く分かりやすく、
ログには詳細で生の情報を残す。

ここが重要です。
エラー情報の整形は、「エラーをきれいに隠す」ためではなく、
「エラーを正しく理解し、正しく伝える」ための技術です。
コードを書くときに、
「このエラーを、ユーザーと自分(開発者)にどう見せたいか?」
と一度立ち止まって考えてみてください。
その問いに答える形で、“整形レイヤー” を少しずつ育てていくと、
非同期エラー処理はぐっと落ち着いたものになっていきます。

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