エラーハンドリングは、アプリケーションが予期せぬ状況を優雅に処理できるようにするプログラミングの基本的な側面です。JavaScriptでは、try-catch
が一般的に使用されていますが、エラーハンドリングを強化するためのさらに高度なテクニックがあります。
この記事では、これらの高度な方法を探求し、エラー管理戦略を改善し、アプリケーションをより堅牢にするための実践的な解決策を提供します。
エラーハンドリングとは何か?
エラーハンドリングの目的
エラーハンドリングは、プログラムの実行中に発生する問題を予測し、検出し、対応するものです。適切なエラーハンドリングはユーザーエクスペリエンスを向上させ、アプリケーションの安定性を維持し、信頼性を確保します。
JavaScriptにおけるエラーの種類
- 構文エラー。これはコードの構文の間違いであり、括弧の欠落やキーワードの誤った使用などが該当します。
- ランタイムエラー。実行中に発生し、未定義のオブジェクトのプロパティにアクセスするなどが含まれます。
- 論理エラー。これらのエラーはプログラムをクラッシュさせるわけではありませんが、誤った結果をもたらし、しばしば欠陥のある論理や意図しない副作用によるものです。
try-catchだけでは不十分な理由
try-catchの制限事項
- スコープの制限。そのブロック内で同期的なコードのみを処理し、明示的に処理されない限り非同期操作には影響しません。
- サイレントな失敗。 過度の使用や不適切な使用は、エラーが静かに無視され、予期しない動作を引き起こす可能性があります。
- エラーの伝播。 アプリケーションの異なるレイヤーを通じてエラーを伝播することをネイティブでサポートしていません。
try-catchを使用するタイミング
- 同期コード。 JSONパースなどの同期操作でエラーを処理するために効果的です。
- クリティカルセクション。 エラーが重大な影響を与える可能性のあるクリティカルコードセクションを保護するために使用します。
カスタムエラークラス: エラー情報の拡張
カスタムエラークラスの作成
カスタムエラークラスは、追加情報を提供するために組込みのError
クラスを拡張します:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.stack = (new Error()).stack; // Capture the stack trace
}
}
カスタムエラーの利点
- 明瞭さ。 特定のエラーメッセージを提供します。
- 粒状な処理。 特定のエラータイプを個別に処理できます。
- エラーメタデータ。 エラーに関する追加のコンテキストを含みます。
カスタムエラーの使用例
- バリデーションの失敗。 ユーザー入力検証に関連するエラー。
- ドメイン固有のエラー。 認証や支払い処理などの特定のアプリケーションドメインに適したエラー。
集中型エラー処理
Node.jsでのグローバルエラー処理
ミドルウェアを使用してエラー処理を集中化:
app.use((err, req, res, next) => {
console.error('Global error handler:', err);
res.status(500).json({ message: 'An error occurred' });
});
フロントエンドアプリケーションでの集中化されたエラー処理
Reactを使用して集中型エラーハンドリングを実装する:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
集中型エラーハンドリングの利点
- 一貫性。エラー管理に対する統一されたアプローチを確保します。
- メンテナンスしやすさ。集中型の更新は変更の見落としのリスクを減らします。
- ログ記録と監視の向上。監視ツールとの統合を容易にします。
エラーの伝播
同期コードにおけるエラーの伝播
エラーを伝播させるためにthrow
を使用します:
function processData(data) {
try {
validateData(data);
saveData(data);
} catch (error) {
console.error('Failed to process data:', error);
throw error;
}
}
非同期コードにおけるエラーの処理
プロミスまたはasync
/await
でエラーを処理します:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
エラーを伝播させるタイミング
- 重要なエラー。アプリケーション全体に影響を与えるエラーを伝播させます。
- ビジネスロジック。上位レベルのコンポーネントがビジネスロジックのエラーを処理できるようにします。
非同期コードにおけるエラーの処理
async/awaitを使用したエラーハンドリング
非同期関数内でエラーを管理するためにtry-catch
を使用します:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
Promise.allとエラーハンドリングの使用
複数のプロミスとエラーを処理します:
async function fetchMultipleData(urls) {
try {
const responses = await Promise.all(urls.map
(url => fetch(url)));
return await Promise.all(responses.map(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${response.url}`);
}
return response.json();
}));
} catch (error) {
console.error('Error fetching multiple data:', error);
return [];
}
}
非同期エラーハンドリングの一般的な落とし穴
- 未処理のプロミス。常に
await
、.then()
、または.catch()
を使用してプロミスを処理します。 - サイレントな失敗。エラーが静かに無視されないようにします。
- 競合状態。並行する非同期操作には注意が必要です。
エラーロギング
クライアント側のエラーロギング
グローバルエラーをキャプチャ:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error captured:', message, source, lineno, colno, error);
sendErrorToService({ message, source, lineno, colno, error });
};
サーバーサイドのエラーロギング
Winstonなどのツールを使用してサーバーサイドのログを取得:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log' })]
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
監視とアラート
PagerDutyやSlackなどのサービスを使用してリアルタイムの監視とアラートを設定:
function notifyError(error) {
// Send error details to monitoring service
}
エラーロギングのベストプラクティス
- コンテキストを含める。リクエストデータやユーザー情報などの追加のコンテキストをログに記録する
- 過度なログ記録を避ける。パフォーマンスの問題を防ぐために必要な情報のみを記録する
- 定期的なログの分析。繰り返し発生する問題を検出して対処するために定期的にログを確認する
優雅な劣化とフォールバック
優雅な劣化
機能を低下させてもアプリケーションが引き続き機能するように設計する:
function renderProfilePicture(user) {
try {
if (!user.profilePicture) {
throw new Error('Profile picture not available');
}
return `<img data-fr-src="${user.profilePicture}" alt="Profile Picture">`;
} catch (error) {
console.error('Error rendering profile picture:', error.message);
return '<img src="/default-profile.png" alt="Default Profile Picture">';
}
}
フォールバックメカニズム
主要な操作が失敗した場合に代替手段を提供する:
async function fetchDataWithFallback(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
return { message: 'Default data' }; // Fallback data
}
}
優雅な劣化の実装
- UIフォールバック。機能が失敗したときに代替のUI要素を提供する
- データフォールバック。ライブデータが利用できない場合にはキャッシュされた値やデフォルト値を使用する
- リトライメカニズム。一時的なエラーのためのリトライロジックを実装する
優雅な劣化のバランス
バランスを保ちながらユーザーに問題を通知すること:
function showErrorNotification(message) {
// Notify users about the issue
}
エラーハンドリングのテスト
ユニットテストエラーハンドリング
個々の関数でエラーハンドリングを検証:
const { validateUserInput } = require('./validation');
test('throws error for invalid username', () => {
expect(() => {
validateUserInput({ username: 'ab' });
}).toThrow('Username must be at least 3 characters long.');
});
統合テスト
異なるアプリケーション層でエラーハンドリングをテスト
test('fetches data with fallback on error', async () => {
fetch.mockReject(new Error('Network error'));
const data = await fetchDataWithFallback('https://api.example.com/data');
expect(data).toEqual({ message: 'Default data' });
});
エンドツーエンドテスト
エラーハンドリングをテストするために実世界のシナリオをシミュレート
describe('ErrorBoundary', () => {
it('displays error message on error', () => {
cy.mount(<ErrorBoundary><MyComponent /></ErrorBoundary>);
cy.get(MyComponent).then(component => {
component.simulateError(new Error('Test error'));
});
cy.contains('Something went wrong.').should('be.visible');
});
});
エラーハンドリングのテストのベストプラクティス
- エッジケースをカバー。さまざまなエラーシナリオに対処するようにテストを行う。
- フォールバックをテスト。フォールバックメカニズムが意図通りに機能することを確認する。
- テストを自動化。CI/CDパイプラインを使用して自動化し、堅牢なエラーハンドリングを確保する。
リアルワールドのシナリオ
シナリオ1:支払処理システム
支払処理中のエラーを処理:
- カスタムエラークラス。
CardValidationError
、PaymentGatewayError
などのクラスを使用。 - リトライロジック。ネットワーク関連の問題に対するリトライを実装する。
- 集中ログ記録。支払エラーを監視し、迅速に問題を解決する。
シナリオ2:データ重視のアプリケーション
データ処理中のエラーを管理:
- 優雅な劣化。部分データや代替ビューを提供する。
- フォールバックデータ。キャッシュされた値やデフォルト値を使用します。
- エラーログ記録。トラブルシューティングのために詳細なコンテキストを記録します。
シナリオ3: ユーザー認証と認可
認証と認可エラーの処理:
- カスタムエラークラス。
AuthenticationError
、AuthorizationError
などのクラスを作成します。 - 中央集権的な処理。認証関連の問題を記録および監視します。
- 優雅な劣化。代替ログインオプションと意味のあるエラーメッセージを提供します。
結論
JavaScriptでの高度なエラー処理には、単純なtry-catch
を超えたカスタムエラー、中央集権的な処理、伝播、堅牢なテストの採用が必要です。これらの技術を実装することで、問題が発生した場合でもシームレスなユーザーエクスペリエンスを提供する頑強なアプリケーションを構築できます。
さらに読む
- 「JavaScript: The Good Parts」by Douglas Crockford
- 「You Don’t Know JS: Async & Performance」by Kyle Simpson
- MDN Web Docs: エラー処理
Source:
https://dzone.com/articles/advanced-error-handling-in-javascript-custom-error