JavaScriptエラーハンドリング入門
try-catchから非同期エラーまで
JavaScriptのエラーハンドリングを基礎から解説。try-catch、カスタムエラー、非同期処理のエラー処理、グローバルハンドリングまで学べます。
こんな人向けの記事です
- JavaScriptのエラー処理を体系的に学びたい
- 非同期処理のエラーハンドリングを理解したい
- カスタムエラークラスを作成したい
Step 1try-catch-finally の基本
JavaScriptでエラーを安全に処理するための基本構文がtry-catch-finallyです。tryブロック内でエラーが発生すると、処理がcatchブロックに移ります。
基本構文
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はリソースの解放や後処理で活躍します。
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を使ってエラーの種類に応じた処理を分岐できます。
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オプションで、エラーの原因を紐づけることができます。
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文を使うと、意図的にエラーを発生させることができます。入力値のバリデーションや、異常な状態を呼び出し元に通知する際に使います。
基本的な使い方
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して呼び出し元に委ねるパターンです。
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クライアントで使える実用的なカスタムエラーの例です。
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のエラーハンドリング
// .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 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.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);
}
});
非同期処理でやりがちなミス
// 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 = 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のエラーキャッチ)
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が完全に壊れないよう、エラー境界を設けるパターンです。
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"));