Node.js エラーハンドリング
1. なぜエラーをハンドリングするのか?
エラーはどんなプログラムにおいても避けられないものですが、その処理(ハンドリング)の仕方がアプリケーションの品質を左右します。Node.js において適切なエラーハンドリングが極めて重要な理由は以下の通りです:
アプリケーションの予期せぬ*クラッシュ(Crash)を防ぐ
- ユーザーに対して意味のあるフィードバックを提供する
- 適切なエラーコンテキスト(Error Context)により、デバッグ(Debugging)を容易にする
- 本番環境におけるアプリケーションの安定性を維持する
- リソース(ファイルディスクリプタやメモリなど)が適切にクリーンアップされることを保証する
2. Node.js における一般的なエラーの種類
異なるエラーの種類を理解することで、それぞれに適切な対処が可能になります:
2.1 標準的な JavaScript エラー
// SyntaxError (構文エラー)
JSON.parse('{invalid json}');
// TypeError (型エラー)
null.someProperty;
// ReferenceError (参照エラー)
unknownVariable;2.2 システムエラー
// ENOENT: ファイルまたはディレクトリが存在しない
const fs = require('fs');
fs.readFile('nonexistent.txt', (err) => {
console.error(err.code); // 'ENOENT'
});
// ECONNREFUSED: 接続が拒否された
const http = require('http');
const req = http.get('http://nonexistent-site.com', (res) => {});
req.on('error', (err) => {
console.error(err.code); // 'ECONNREFUSED' または 'ENOTFOUND'
});3. 基本的なエラーハンドリング
Node.js は、エラーハンドリングのためにいくつかのパターンに従います:
3.1 エラー優先コールバック (Error-First Callbacks)
Node.js のコアモジュール(Module)で最も一般的なパターンであり、コールバックの最初の引数がエラーオブジェクト(発生した場合)になります。
例:エラー優先コールバック
const fs = require('fs');
function readConfigFile(filename, callback) {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
// 特定のエラータイプをハンドリング
if (err.code === 'ENOENT') {
return callback(new Error(`設定ファイル ${filename} が見つかりません`));
} else if (err.code === 'EACCES') {
return callback(new Error(`${filename} を読み取る権限がありません`));
}
// その他のすべてのエラー
return callback(err);
}
// エラーがない場合にデータを処理
try {
const config = JSON.parse(data);
callback(null, config);
} catch (parseError) {
callback(new Error(`${filename} 内の JSON が無効です`));
}
});
}
// 使用例
readConfigFile('config.json', (err, config) => {
if (err) {
console.error('設定の読み込みに失敗しました:', err.message);
// エラー処理(例:デフォルト設定を使用するなど)
return;
}
console.log('設定が正常にロードされました:', config);
});4. モダンなエラーハンドリング
4.1 Async/Await と try...catch の併用
Async/Await を使用すると、同期コードと非同期コードの両方に対して try/catch ブロックを使用でき、可読性が高まります。
例:Async/Await による try/catch
const fs = require('fs').promises;
async function loadUserData(userId) {
try {
const data = await fs.readFile(`users/${userId}.json`, 'utf8');
const user = JSON.parse(data);
if (!user.email) {
throw new Error('無効なユーザーデータ: メールアドレスがありません');
}
return user;
} catch (error) {
// 異なるエラータイプをハンドリング
if (error.code === 'ENOENT') {
throw new Error(`ユーザー ${userId} が見つかりません`);
} else if (error instanceof SyntaxError) {
throw new Error('ユーザーデータのフォーマットが無効です');
}
// その他のエラーは再スロー
throw error;
} finally {
// 成功・失敗に関わらず実行されるクリーンアップコード
console.log(`ユーザー ${userId} の処理が終了しました`);
}
}
// 使用例
(async () => {
try {
const user = await loadUserData(123);
console.log('ユーザーをロードしました:', user);
} catch (error) {
console.error('ユーザーのロードに失敗しました:', error.message);
// エラー処理(ユーザーへの通知、リトライなど)
}
})();5. グローバルなエラーハンドリング
5.1 未捕捉の例外 (Uncaught Exceptions)
予期せぬエラーに対しては、uncaughtException イベントをリッスンして、プロセスを終了する前にクリーンアップを実行できます。
例:グローバルエラーハンドラー
// 未捕捉の例外(同期エラー)をハンドリング
process.on('uncaughtException', (error) => {
console.error('未捕捉の例外 (UNCAUGHT EXCEPTION) 発生! シャットダウンします...');
console.error(error.name, error.message);
// クリーンアップの実行(データベース接続の切断など)
server.close(() => {
console.log('未捕捉の例外によりプロセスを終了しました');
process.exit(1); // 異常終了
});
});
// ハンドルされていないプロミス拒否 (Unhandled Rejection) をハンドリング
process.on('unhandledRejection', (reason, promise) => {
console.error('ハンドルされていない拒否 (UNHANDLED REJECTION) 発生! シャットダウンします...');
console.error('場所:', promise, '理由:', reason);
// サーバーを閉じて終了
server.close(() => {
process.exit(1);
});
});
// ハンドルされていないプロミス拒否の例
Promise.reject(new Error('何かが失敗しました'));
// 未捕捉の例外の例
setTimeout(() => {
throw new Error('タイムアウト後に発生した未捕捉の例外');
}, 1000);6. エラーハンドリングのベストプラクティス
6.1 推奨事項(Do)と避けるべき事項(Don't)
| カテゴリ | 推奨事項 (Do) | 避けるべき事項 (Don't) |
|---|---|---|
| レベル | 適切なレベルでエラーを処理する | エラーを無視する(空の catch ブロック) |
| ロギング | 十分なコンテキストと共にエラーをログ出力する | 機密性の高いエラー詳細をクライアントに公開する |
| カスタム | シナリオごとにカスタムエラータイプを使用する | 制御フローのために try/catch を使用する |
| クリーンアップ | finally ブロックでリソースをクリーンアップする | ログ出力せずにエラーを握りつぶす |
| 検証 | 入力値を検証してエラーを早期にキャッチする | 回復不可能なエラーの後も実行を継続する |
7. カスタムエラータイプ
独自のクラス(Class)を作成することで、エラーの分類と処理がより明確になります。
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.statusCode = 400; // 不正なリクエスト
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} が見つかりません`);
this.name = 'NotFoundError';
this.statusCode = 404; // 未検出
}
}
// 使用例
function getUser(id) {
if (!id) {
throw new ValidationError('ユーザーIDは必須です', 'id');
}
// ...処理
}8. まとめ
効果的なエラーハンドリングは、堅牢な Node.js アプリケーションを構築する上で不可欠な要素です。
異なるエラーの種類を理解し、適切なパターンを使用し、ベストプラクティスに従うことで、より安定し、メンテナンス性が高く、ユーザーフレンドリーなアプリケーションを作成できます。
優れたエラーハンドリングとは、単にクラッシュを防ぐことだけではありません。意味のあるフィードバックを提供し、データの整合性を維持し、問題が発生した際にも良好なユーザーエクスペリエンス(UX)を保証することなのです。