基礎

JavaScriptエラーハンドリング入門|try-catchから非同期エラーまで

JavaScript エラー処理 デバッグ

JavaScriptエラーハンドリング入門
try-catchから非同期エラーまで

JavaScriptのエラーハンドリングを基礎から解説。try-catch、カスタムエラー、非同期処理のエラー処理、グローバルハンドリングまで学べます。

こんな人向けの記事です

  • JavaScriptのエラー処理を体系的に学びたい
  • 非同期処理のエラーハンドリングを理解したい
  • カスタムエラークラスを作成したい

Step 1try-catch-finally の基本

JavaScriptでエラーを安全に処理するための基本構文がtry-catch-finallyです。tryブロック内でエラーが発生すると、処理がcatchブロックに移ります。

基本構文

try-catch-finally の構文
try {
  // エラーが発生する可能性のある処理
  const data = JSON.parse('{"name": "太郎"}');
  console.log(data.name); // "太郎"
} catch (error) {
  // エラーが発生した場合の処理
  console.error("パースに失敗:", error.message);
} finally {
  // エラーの有無に関わらず必ず実行される
  console.log("処理完了");
}

各ブロックの役割を整理しましょう。

ブロック 実行タイミング 省略
try 常に実行される(エラーが出たら中断) 不可
catch tryでエラーが発生した場合のみ finallyがあれば可
finally エラーの有無に関わらず必ず実行 catchがあれば可

catchでエラー情報を取得する

エラー情報の取得
try {
  const result = undefinedVariable * 2;
} catch (error) {
  console.log(error.name);    // "ReferenceError"
  console.log(error.message); // "undefinedVariable is not defined"
  console.log(error.stack);   // スタックトレース(デバッグに有用)
}
ポイント

error.stackはエラーが発生した場所の呼び出し履歴を含みます。デバッグ時に最も役立つプロパティです。本番環境ではスタックトレースをユーザーに表示せず、ログに記録するようにしましょう。

finallyの活用例

finallyはリソースの解放や後処理で活躍します。

finallyでローディング表示を制御
function fetchData() {
  const spinner = document.getElementById("loading");
  spinner.style.display = "block";

  try {
    const data = JSON.parse(getSomeData());
    renderData(data);
  } catch (error) {
    showErrorMessage("データの取得に失敗しました");
  } finally {
    // 成功でも失敗でもローディングを非表示にする
    spinner.style.display = "none";
  }
}
注意

try-catchは同期処理のエラーのみをキャッチします。setTimeoutやPromise内で発生したエラーはキャッチできません。非同期処理のエラーハンドリングについてはStep 5で解説します。

Step 2Error オブジェクトの種類

JavaScriptには用途に応じた複数の組み込みエラークラスがあります。すべてErrorを基底クラスとして継承しています。

主要なエラータイプ一覧

エラー型 発生条件
TypeError 値の型が期待と異なる null.toString()
ReferenceError 未定義の変数を参照 console.log(x)(xが未定義)
SyntaxError 構文が正しくない JSON.parse("{'bad'}")
RangeError 値が許容範囲外 new Array(-1)
URIError URI関数の引数が不正 decodeURI("%")
EvalError eval()関連のエラー(現在はほぼ使われない)

エラータイプによる分岐処理

instanceofを使ってエラーの種類に応じた処理を分岐できます。

instanceof でエラータイプを判定
try {
  // 何らかの処理
  const config = JSON.parse(userInput);
  config.validate();
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error("JSON形式が不正です:", error.message);
  } else if (error instanceof TypeError) {
    console.error("型エラー:", error.message);
  } else {
    console.error("予期しないエラー:", error);
  }
}
ポイント

error.nameで文字列比較するより、instanceofを使う方が安全です。カスタムエラークラスでも正しく判定でき、継承関係も考慮されます。

エラーオブジェクトの共通プロパティ

プロパティ 説明 用途
name エラーの名前("TypeError" 等) ログ出力・分類
message エラーの詳細メッセージ ユーザー通知・デバッグ
stack スタックトレース デバッグ・エラー追跡
cause エラーの原因(ES2022〜) エラーチェーン

エラーチェーン(cause)

ES2022から導入されたcauseオプションで、エラーの原因を紐づけることができます。

cause でエラーの原因を記録
try {
  try {
    JSON.parse("invalid json");
  } catch (parseError) {
    throw new Error("設定ファイルの読み込みに失敗", {
      cause: parseError
    });
  }
} catch (error) {
  console.log(error.message);       // "設定ファイルの読み込みに失敗"
  console.log(error.cause.message); // "Unexpected token..."
}

Step 3throw でエラーを発生させる

throw文を使うと、意図的にエラーを発生させることができます。入力値のバリデーションや、異常な状態を呼び出し元に通知する際に使います。

基本的な使い方

throw で入力値を検証
function divide(a, b) {
  if (typeof a !== "number" || typeof b !== "number") {
    throw new TypeError("引数は数値である必要があります");
  }
  if (b === 0) {
    throw new RangeError("0で割ることはできません");
  }
  return a / b;
}

try {
  console.log(divide(10, 0));
} catch (error) {
  console.error(`${error.name}: ${error.message}`);
  // "RangeError: 0で割ることはできません"
}
注意

throw "エラーです"のように文字列を投げることも可能ですが、推奨されません。Errorオブジェクトを使わないとstackプロパティが取得できず、デバッグが困難になります。

バリデーション関数のパターン

ユーザー入力のバリデーション
function validateUser(user) {
  if (!user) {
    throw new Error("ユーザーデータが必要です");
  }
  if (!user.name || user.name.trim() === "") {
    throw new Error("名前は必須です");
  }
  if (typeof user.age !== "number" || user.age < 0 || user.age > 150) {
    throw new RangeError("年齢は0〜150の数値で指定してください");
  }
  return true;
}

// 使用例
try {
  validateUser({ name: "", age: 25 });
} catch (error) {
  console.error(error.message); // "名前は必須です"
}

再throw(エラーの再送出)

catchで特定のエラーだけ処理し、それ以外は再throwして呼び出し元に委ねるパターンです。

特定のエラーだけ処理して残りは再throw
function processJSON(jsonString) {
  try {
    const data = JSON.parse(jsonString);
    return data.value.toUpperCase();
  } catch (error) {
    if (error instanceof SyntaxError) {
      // JSON構文エラーは自分で処理
      console.error("無効なJSON形式です");
      return null;
    }
    // それ以外のエラー(TypeErrorなど)は再throw
    throw error;
  }
}
ポイント

再throwは「関心の分離」に役立ちます。各関数は自分が対処できるエラーだけを処理し、対処できないエラーは上位に伝播させるのがベストプラクティスです。

Step 4カスタムエラークラス

組み込みのErrorクラスを継承して、アプリケーション固有のエラークラスを作成できます。エラーの種類を明確に区別でき、コードの可読性が向上します。

基本的なカスタムエラー

カスタムエラークラスの定義
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;  // 追加のプロパティ
  }
}

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource}(ID: ${id})が見つかりません`);
    this.name = "NotFoundError";
    this.resource = resource;
    this.resourceId = id;
  }
}

// 使用例
function findUser(id) {
  const user = database.get(id);
  if (!user) {
    throw new NotFoundError("ユーザー", id);
  }
  return user;
}

try {
  findUser(999);
} catch (error) {
  if (error instanceof NotFoundError) {
    console.error(error.message); // "ユーザー(ID: 999)が見つかりません"
    console.log(error.resource);  // "ユーザー"
  }
}

HTTPエラークラスの実装例

APIクライアントで使える実用的なカスタムエラーの例です。

HTTPエラークラス
class HttpError extends Error {
  constructor(status, statusText, url) {
    super(`HTTP ${status}: ${statusText}`);
    this.name = "HttpError";
    this.status = status;
    this.statusText = statusText;
    this.url = url;
  }

  get isClientError() {
    return this.status >= 400 && this.status < 500;
  }

  get isServerError() {
    return this.status >= 500;
  }
}

// API呼び出しでの使用
async function fetchAPI(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new HttpError(response.status, response.statusText, url);
  }
  return response.json();
}

try {
  const data = await fetchAPI("/api/users");
} catch (error) {
  if (error instanceof HttpError) {
    if (error.status === 404) {
      showNotFound();
    } else if (error.isServerError) {
      showServerError();
    }
  }
}

エラークラスの設計パターン

カスタムエラー 用途 追加プロパティ例
ValidationError 入力値の検証エラー field, value, rule
NotFoundError リソースが存在しない resource, resourceId
AuthenticationError 認証に失敗 reason
NetworkError 通信エラー url, retryCount
HttpError HTTPステータスエラー status, statusText
ポイント

カスタムエラーではthis.nameをクラス名と一致させましょう。console.errorやスタックトレースでエラー名が表示されるため、デバッグが格段に楽になります。

Step 5非同期処理のエラーハンドリング

非同期処理では通常のtry-catchだけではエラーをキャッチできません。Promiseの.catch()やasync/awaitのtry-catchを使う必要があります。

Promiseのエラーハンドリング

Promise の .catch() メソッド
// .then().catch() チェーン
fetch("/api/users")
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    console.log("ユーザー一覧:", data);
  })
  .catch(error => {
    // ネットワークエラーもHTTPエラーもここでキャッチ
    console.error("取得に失敗:", error.message);
  })
  .finally(() => {
    // 成功・失敗に関わらず実行
    hideLoadingSpinner();
  });
注意

.catch()を付け忘れると、エラーがUnhandled Promise Rejectionとなります。Node.jsではプロセスが終了する原因にもなるため、Promiseには必ず.catch()を付けましょう。

async/await のエラーハンドリング

async/awaitでは同期処理と同じようにtry-catchが使えます。

async/await + try-catch
async function getUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      throw new HttpError(response.status, response.statusText, response.url);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    if (error instanceof HttpError) {
      console.error(`APIエラー(${error.status}):`, error.message);
    } else {
      // ネットワークエラーなど
      console.error("通信エラー:", error.message);
    }
    return null;
  }
}

複数の非同期処理のエラーハンドリング

Promise.allSettled で全結果を取得
// Promise.all は1つでも失敗すると全体が失敗
try {
  const [users, posts] = await Promise.all([
    fetch("/api/users").then(r => r.json()),
    fetch("/api/posts").then(r => r.json()),
  ]);
} catch (error) {
  console.error("いずれかのAPIが失敗:", error.message);
}

// Promise.allSettled は全結果を返す(失敗も含む)
const results = await Promise.allSettled([
  fetch("/api/users").then(r => r.json()),
  fetch("/api/posts").then(r => r.json()),
]);

results.forEach((result, i) => {
  if (result.status === "fulfilled") {
    console.log(`API ${i}: 成功`, result.value);
  } else {
    console.error(`API ${i}: 失敗`, result.reason.message);
  }
});

非同期処理でやりがちなミス

try-catch で非同期エラーがキャッチできないパターン
// NG: setTimeout内のエラーはキャッチできない
try {
  setTimeout(() => {
    throw new Error("このエラーはキャッチされない");
  }, 1000);
} catch (error) {
  // ここには到達しない
}

// OK: Promiseでラップすればキャッチできる
function delay(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        // エラーが発生する処理
        resolve();
      } catch (error) {
        reject(error);
      }
    }, ms);
  });
}

try {
  await delay(1000);
} catch (error) {
  console.error("キャッチできた:", error.message);
}

Step 6グローバルエラーハンドリング

個々のtry-catchで補足しきれないエラーに対して、アプリケーション全体で最後の砦となるグローバルエラーハンドラーを設定できます。

window.onerror(同期エラーのキャッチ)

window.onerror でキャッチされなかったエラーを処理
window.onerror = function(message, source, lineno, colno, error) {
  console.error("グローバルエラー検出:");
  console.error("  メッセージ:", message);
  console.error("  ファイル:", source);
  console.error("  行番号:", lineno);
  console.error("  列番号:", colno);
  console.error("  エラー:", error);

  // エラーログをサーバーに送信
  sendErrorLog({
    type: "uncaught",
    message,
    source,
    lineno,
    colno,
    stack: error?.stack,
  });

  // true を返すとデフォルトのエラー表示を抑制
  return true;
};

unhandledrejection(Promiseのエラーキャッチ)

unhandledrejection で未処理のPromiseエラーをキャッチ
window.addEventListener("unhandledrejection", (event) => {
  console.error("未処理のPromiseエラー:", event.reason);

  // エラーログをサーバーに送信
  sendErrorLog({
    type: "unhandledrejection",
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
  });

  // デフォルトの動作(コンソールエラー)を抑制
  event.preventDefault();
});

エラーログ送信の実装例

エラー情報をサーバーに送信するユーティリティ
function sendErrorLog(errorInfo) {
  // navigator.sendBeacon はページ離脱時でも確実に送信できる
  const payload = JSON.stringify({
    ...errorInfo,
    url: location.href,
    userAgent: navigator.userAgent,
    timestamp: new Date().toISOString(),
  });

  // sendBeaconが使えればそちらを優先(ページ離脱に強い)
  if (navigator.sendBeacon) {
    navigator.sendBeacon("/api/error-log", payload);
  } else {
    fetch("/api/error-log", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: payload,
    }).catch(() => {
      // ログ送信自体のエラーは無視
    });
  }
}
ポイント

グローバルエラーハンドラーは「最後の砦」です。本来のエラー処理は各関数のtry-catchで行い、グローバルハンドラーは未処理エラーの検知・ログ記録に限定するのがベストプラクティスです。

ErrorBoundaryパターン(UI保護)

エラーが発生してもUIが完全に壊れないよう、エラー境界を設けるパターンです。

エラー境界でUIを保護する
function safeRender(renderFn, container, fallbackMessage) {
  try {
    renderFn(container);
  } catch (error) {
    console.error("描画エラー:", error);
    container.innerHTML = `
      <div class="error-fallback">
        <p>${fallbackMessage || "コンテンツの表示でエラーが発生しました"}</p>
        <button onclick="location.reload()">ページを再読み込み</button>
      </div>
    `;
  }
}

// 使用例
safeRender(renderUserProfile, document.getElementById("profile"));
safeRender(renderDashboard, document.getElementById("dashboard"));

Checkエラーハンドリング チェックリスト