JavaScriptエラーハンドリングの実践 - 堅牢なコードを書く方法
「動いたから完成!」と思ってリリースしたら、ユーザーから「エラーが出て使えない」と報告される——そんな経験はありませんか?エラーハンドリング(エラー処理)は、堅牢なWebアプリを作る上で不可欠なスキルです。
この記事では、MechaToraで15個のツールを運用する中で学んだ、JavaScriptのエラーハンドリングとデバッグの実践テクニックを紹介します。基本的なtry-catchから、非同期処理のエラー対処、本番環境でのエラー監視まで、実務で使える内容をお伝えします。
エラーハンドリングが重要な理由
放置すると起こる問題
私がエラーハンドリングを軽視していた頃、以下のような問題が発生しました:
- アプリが突然停止:APIエラーでページ全体が固まる
- ユーザー体験の悪化:何が起きたか分からず、ユーザーが困惑
- デバッグが困難:エラー原因が特定できない
- セキュリティリスク:エラーメッセージで内部情報が露出
適切なエラーハンドリングの効果
- 予期しない動作を防ぐ:エラーが起きても最低限の機能は維持
- ユーザーフレンドリー:分かりやすいエラーメッセージを表示
- デバッグが容易:ログで原因を特定しやすい
- 保守性向上:エラーが一箇所に集約され、管理しやすい
基本:try-catch文
基本的な使い方
最も基本的なエラーハンドリングがtry-catch文です:
try {
// エラーが発生する可能性のあるコード
const data = JSON.parse(jsonString);
console.log(data);
} catch (error) {
// エラーが発生したときの処理
console.error('JSONのパースに失敗:', error.message);
alert('データの読み込みに失敗しました');
}
finally句の活用
エラーの有無に関わらず実行したい処理は、finally句に書きます:
function loadData() {
showLoading(); // ローディング表示
try {
const data = fetchDataFromAPI();
displayData(data);
} catch (error) {
showError('データ取得に失敗しました');
} finally {
hideLoading(); // 必ずローディングを消す
}
}
実際の活用例:LocalStorageの読み込み
MechaToraの各ツールで使っている実装:
function loadSettings() {
try {
const settingsJson = localStorage.getItem('app_settings');
if (!settingsJson) {
return getDefaultSettings(); // デフォルト値を返す
}
const settings = JSON.parse(settingsJson);
return settings;
} catch (error) {
console.error('設定の読み込みエラー:', error);
// エラー時もデフォルト値で動作を継続
return getDefaultSettings();
}
}
function getDefaultSettings() {
return {
theme: 'light',
language: 'ja',
notifications: true
};
}
非同期処理のエラーハンドリング
Promise の .catch()
Promiseベースの非同期処理では、.catch()でエラーを捕捉します:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('データ取得成功:', data);
})
.catch(error => {
console.error('データ取得失敗:', error);
showError('通信エラーが発生しました。時間をおいて再度お試しください。');
});
async/await での try-catch
async/awaitを使うと、同期処理と同じようにtry-catchが使えます(推奨):
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('ユーザーデータ取得エラー:', error);
// ネットワークエラーか HTTPエラーかで分岐
if (error.message.includes('Failed to fetch')) {
throw new Error('ネットワークに接続できません');
} else {
throw new Error('ユーザー情報の取得に失敗しました');
}
}
}
実際の活用例:楽天API呼び出し
日本酒検索アプリでの実装:
async function searchSake(keyword) {
const API_KEY = 'YOUR_API_KEY';
const url = `https://app.rakuten.co.jp/services/api/IchibaItem/Search/20220601?
applicationId=${API_KEY}&
keyword=${encodeURIComponent(keyword + ' 日本酒')}&
hits=30`;
try {
showLoading();
const response = await fetch(url);
// HTTPエラーチェック
if (!response.ok) {
if (response.status === 429) {
throw new Error('API利用制限に達しました。しばらく経ってから再度お試しください。');
} else if (response.status === 404) {
throw new Error('APIエンドポイントが見つかりません。');
} else {
throw new Error(`APIエラー: ${response.status}`);
}
}
const data = await response.json();
// データの検証
if (!data.Items || data.Items.length === 0) {
showMessage('該当する商品が見つかりませんでした');
return [];
}
return data.Items;
} catch (error) {
console.error('検索エラー:', error);
// ユーザーフレンドリーなエラーメッセージ
if (error.message.includes('Failed to fetch')) {
showError('ネットワークエラーが発生しました。インターネット接続を確認してください。');
} else {
showError(error.message);
}
return [];
} finally {
hideLoading();
}
}
カスタムエラークラス
独自のエラー型を定義
エラーの種類を明確にするため、カスタムエラークラスを作ります:
// ネットワークエラー
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
// APIエラー
class APIError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
}
}
// バリデーションエラー
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
カスタムエラーの活用
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new APIError(
`API request failed: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
throw new NetworkError('ネットワークに接続できません');
}
throw error;
}
}
// 使用例
try {
const data = await fetchData('https://api.example.com/data');
console.log(data);
} catch (error) {
if (error instanceof NetworkError) {
showNetworkErrorDialog();
} else if (error instanceof APIError) {
showAPIErrorDialog(error.statusCode);
} else {
showGeneralErrorDialog();
}
}
グローバルエラーハンドラー
window.onerror
キャッチされなかったエラーを一箇所で処理:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error:', {
message,
source,
lineno,
colno,
error
});
// エラーをサーバーに送信(本番環境)
if (location.hostname !== 'localhost') {
logErrorToServer({
message,
source,
lineno,
colno,
stack: error?.stack,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
});
}
// ユーザーに通知
showErrorNotification('予期しないエラーが発生しました');
// エラーの伝播を止める
return true;
};
Promiseの未処理エラーを捕捉
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
// エラーログ送信
logErrorToServer({
type: 'unhandledRejection',
reason: event.reason,
promise: event.promise,
timestamp: new Date().toISOString()
});
// ユーザーに通知
showErrorNotification('処理中にエラーが発生しました');
// デフォルトの動作を防ぐ
event.preventDefault();
});
デバッグテクニック
1. console.log の活用
基本ですが、使い分けが重要:
// 通常のログ
console.log('変数の値:', myVariable);
// 警告
console.warn('この機能は非推奨です');
// エラー
console.error('APIエラー:', error);
// テーブル形式(配列・オブジェクト)
console.table(users);
// グループ化
console.group('ユーザー情報');
console.log('名前:', user.name);
console.log('年齢:', user.age);
console.groupEnd();
// 実行時間の計測
console.time('データ取得');
await fetchData();
console.timeEnd('データ取得'); // "データ取得: 234.56ms"
2. デバッガーの使用
Chrome DevTools のデバッガーを活用:
function complexCalculation(data) {
debugger; // この行で実行が一時停止
let result = 0;
for (let item of data) {
result += item.value * item.quantity;
}
return result;
}
DevToolsの便利機能:
- ブレークポイント:行番号クリックで設定
- 条件付きブレークポイント:特定条件のみ停止
- ステップ実行:F10(次の行)、F11(関数内に入る)
- Watch式:変数の値を監視
- Call Stack:関数の呼び出し履歴を確認
3. エラーの再現方法
意図的にエラーを発生させてテスト:
// ネットワークエラーをシミュレート
async function fetchDataWithSimulatedError(url, shouldFail = false) {
if (shouldFail) {
throw new Error('Simulated network error');
}
return await fetch(url);
}
// APIレート制限をシミュレート
function mockAPIResponse(shouldReturn429 = false) {
if (shouldReturn429) {
return new Response(null, { status: 429 });
}
return new Response(JSON.stringify({ data: 'success' }), { status: 200 });
}
本番環境でのエラー監視
シンプルなエラーログ送信
サーバーにエラーを送信して記録:
async function logErrorToServer(errorInfo) {
try {
await fetch('https://your-server.com/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(errorInfo)
});
} catch (error) {
// エラーログの送信が失敗しても、アプリは止めない
console.error('Failed to log error:', error);
}
}
// 使用例
try {
// 何らかの処理
} catch (error) {
logErrorToServer({
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
});
}
外部サービスの活用
本格的な監視には、以下のようなサービスが便利です:
- Sentry:無料枠あり、エラートラッキングの定番
- LogRocket:セッションリプレイ機能付き
- Google Analytics:イベントトラッキングでエラーを記録
Sentryの簡単な導入例:
// Sentryをインストール
// npm install @sentry/browser
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
environment: "production",
// サンプリングレート(エラーの10%のみ送信)
sampleRate: 0.1
});
// エラーを手動で送信
try {
// 何らかの処理
} catch (error) {
Sentry.captureException(error);
}
実践的なエラーハンドリングパターン
パターン1:Fetch ラッパー関数
全てのAPI呼び出しで統一したエラーハンドリング:
async function apiFetch(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new APIError(
errorData.message || `HTTP ${response.status}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
throw error;
}
// ネットワークエラー
throw new NetworkError('ネットワークエラーが発生しました');
}
}
// 使用例
try {
const data = await apiFetch('https://api.example.com/data');
console.log(data);
} catch (error) {
if (error instanceof NetworkError) {
showError('インターネット接続を確認してください');
} else if (error instanceof APIError && error.statusCode === 404) {
showError('データが見つかりませんでした');
} else {
showError('エラーが発生しました');
}
}
パターン2:リトライロジック付きFetch
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// 429エラー(Rate Limit)は再試行
if (response.status === 429 && i < retries - 1) {
const retryAfter = response.headers.get('Retry-After') || 2;
await sleep(retryAfter * 1000);
continue;
}
throw new APIError(`HTTP ${response.status}`, response.status);
} catch (error) {
// 最後の試行で失敗したらエラーを投げる
if (i === retries - 1) {
throw error;
}
// 指数バックオフ(1秒、2秒、4秒...)
await sleep(Math.pow(2, i) * 1000);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
まとめ
堅牢なJavaScriptアプリを作るためのエラーハンドリングのポイント:
基本原則
- 全ての非同期処理にエラーハンドリング:try-catch または .catch()
- ユーザーフレンドリーなメッセージ:技術的な詳細は隠す
- エラーをログに記録:デバッグのため
- グレースフルデグラデーション:エラー時も最低限の機能は維持
実装チェックリスト
- ✅ API呼び出しにtry-catchを実装
- ✅ HTTPステータスコードを適切に処理
- ✅ ネットワークエラーを考慮
- ✅ グローバルエラーハンドラーを設定
- ✅ 本番環境でエラーログを記録
- ✅ ローディング表示をfinallyで確実に消す
- ✅ ユーザーに分かりやすいエラーメッセージ
MechaToraの15個のツールでは、これらのエラーハンドリングを実装することで、ユーザーからのエラー報告が劇的に減りました。最初は面倒に感じるかもしれませんが、後々のデバッグコストを考えると、必ず実装すべきです。
エラーハンドリングは、ユーザー体験を守るための「保険」です。ぜひ、今日から実装してみてください!