Node.js エラー処理パターン:try/catch・エラーハンドラー徹底解説

Node.js アプリケーション開発において、エラー処理は単なる例外対応を超えた、アプリケーションの信頼性と保守性を決定する重要な要素です。適切なエラーハンドリングが実装されていないアプリケーションは、予期しないクラッシュやサイレントエラーにより、ユーザー体験の悪化やデータ損失のリスクを抱えることになります。
特に Node.js は非同期処理が中心的な役割を果たすため、従来の同期的なエラー処理手法だけでは不十分です。Promise、async/await、EventEmitter、さらにはグローバルレベルでのエラーハンドリングまで、多層的なアプローチが求められます。
本記事では、Node.js における包括的なエラー処理パターンを体系的に解説し、実際のエラーコードとその対処法を具体的に示しながら、堅牢なアプリケーション構築のための実践的な知識を提供いたします。基本的な try/catch から運用レベルの監視戦略まで、段階的に理解を深められる構成となっています。
基本のエラー処理:try/catch の正しい使い方
Node.js でのエラー処理の基礎は、JavaScript の try/catch 構文から始まります。しかし、非同期処理が主体の Node.js では、単純な try/catch の使用には注意が必要です。
同期処理での try/catch 実装
同期処理においては、try/catch は期待通りに動作します。基本的な使用法から、より実践的なパターンまで見ていきましょう。
javascript// 基本的な try/catch の使用例
function parseConfigSync(configString) {
try {
const config = JSON.parse(configString);
// 設定値の検証
if (!config.port || typeof config.port !== 'number') {
throw new Error('Invalid port configuration');
}
if (!config.host || typeof config.host !== 'string') {
throw new Error('Invalid host configuration');
}
console.log('設定ファイルの解析が完了しました');
return config;
} catch (error) {
// エラータイプ別の処理
if (error instanceof SyntaxError) {
console.error('JSON形式エラー:', error.message);
throw new Error(
`設定ファイルの形式が正しくありません: ${error.message}`
);
}
if (error.message.includes('Invalid')) {
console.error('設定値エラー:', error.message);
throw error; // カスタムエラーはそのまま再投
}
// 予期しないエラー
console.error('予期しないエラーが発生しました:', error);
throw new Error(
'設定ファイルの処理中にエラーが発生しました'
);
}
}
// 使用例
try {
const config = parseConfigSync(
'{"port": 3000, "host": "localhost"}'
);
console.log('設定:', config);
} catch (error) {
console.error(
'アプリケーション起動エラー:',
error.message
);
process.exit(1);
}
エラーの詳細情報取得と分類
エラー情報を適切に分類し、デバッグに役立つ詳細な情報を取得する方法を実装します。
javascript// fs モジュールを使用したファイル処理でのエラーハンドリング
const fs = require('fs');
const path = require('path');
class FileProcessor {
static readFileSync(filePath) {
try {
// ファイルの存在確認
if (!fs.existsSync(filePath)) {
throw new Error(
`ENOENT: no such file or directory, open '${filePath}'`
);
}
const stats = fs.statSync(filePath);
// ディレクトリかどうかの確認
if (stats.isDirectory()) {
throw new Error(
`EISDIR: illegal operation on a directory, read '${filePath}'`
);
}
// ファイルサイズの確認(例:100MB制限)
const maxSize = 100 * 1024 * 1024; // 100MB
if (stats.size > maxSize) {
throw new Error(
`File size too large: ${stats.size} bytes (max: ${maxSize})`
);
}
const content = fs.readFileSync(filePath, 'utf8');
console.log(
`ファイル読み込み成功: ${filePath} (${stats.size} bytes)`
);
return content;
} catch (error) {
// Node.js の標準エラーコード別処理
switch (error.code) {
case 'ENOENT':
console.error(
`ファイルが見つかりません: ${filePath}`
);
throw new Error(
`Required file not found: ${filePath}`
);
case 'EACCES':
console.error(
`ファイルへのアクセス権限がありません: ${filePath}`
);
throw new Error(`Permission denied: ${filePath}`);
case 'EISDIR':
console.error(
`指定されたパスはディレクトリです: ${filePath}`
);
throw new Error(
`Expected file but got directory: ${filePath}`
);
case 'EMFILE':
console.error(
'開けるファイル数の上限に達しました'
);
throw new Error('Too many open files');
case 'ENOTDIR':
console.error(
`パスの一部がディレクトリではありません: ${filePath}`
);
throw new Error(
`Invalid path structure: ${filePath}`
);
default:
// カスタムエラーまたは予期しないエラー
if (
error.message.includes('File size too large')
) {
throw error; // カスタムエラーはそのまま
}
console.error(
'ファイル処理で予期しないエラー:',
error
);
throw new Error(
`File processing failed: ${error.message}`
);
}
}
}
// 複数ファイルの一括処理
static processBatchFiles(filePaths) {
const results = [];
const errors = [];
for (const filePath of filePaths) {
try {
const content = this.readFileSync(filePath);
results.push({
filePath,
success: true,
content: content.substring(0, 100) + '...', // プレビュー
size: content.length,
});
} catch (error) {
errors.push({
filePath,
success: false,
error: error.message,
timestamp: new Date().toISOString(),
});
}
}
return {
processed: results.length,
failed: errors.length,
results,
errors,
};
}
}
// 使用例
const filesToProcess = [
'./config.json',
'./package.json',
'./nonexistent.txt',
'./logs',
];
try {
const batchResult =
FileProcessor.processBatchFiles(filesToProcess);
console.log('=== バッチ処理結果 ===');
console.log(`成功: ${batchResult.processed}件`);
console.log(`失敗: ${batchResult.failed}件`);
if (batchResult.errors.length > 0) {
console.log('\n=== エラー詳細 ===');
batchResult.errors.forEach((error) => {
console.error(`❌ ${error.filePath}: ${error.error}`);
});
}
} catch (error) {
console.error('バッチ処理で致命的エラー:', error.message);
}
finally ブロックによるリソース管理
リソースの適切な解放を保証するために、finally ブロックを活用した実装を行います。
javascriptconst fs = require('fs');
class ResourceManager {
// ファイルハンドルの安全な管理
static processFileWithResource(filePath) {
let fileDescriptor = null;
try {
console.log(`ファイル処理開始: ${filePath}`);
// ファイルを開く
fileDescriptor = fs.openSync(filePath, 'r');
// ファイル情報の取得
const stats = fs.fstatSync(fileDescriptor);
console.log(`ファイルサイズ: ${stats.size} bytes`);
// ファイル内容の読み込み(チャンク単位)
const bufferSize = 1024;
const buffer = Buffer.alloc(bufferSize);
let totalBytesRead = 0;
let position = 0;
while (position < stats.size) {
const bytesRead = fs.readSync(
fileDescriptor,
buffer,
0,
bufferSize,
position
);
if (bytesRead === 0) break;
totalBytesRead += bytesRead;
position += bytesRead;
// 進捗表示
const progress = (
(position / stats.size) *
100
).toFixed(2);
console.log(`読み込み進捗: ${progress}%`);
}
console.log(
`ファイル処理完了: ${totalBytesRead} bytes 読み込み`
);
return totalBytesRead;
} catch (error) {
console.error('ファイル処理エラー:', error.message);
// エラーの詳細分析
if (error.code === 'ENOENT') {
throw new Error(`File not found: ${filePath}`);
} else if (error.code === 'EACCES') {
throw new Error(`Permission denied: ${filePath}`);
} else {
throw new Error(
`File processing failed: ${error.message}`
);
}
} finally {
// リソースの確実な解放
if (fileDescriptor !== null) {
try {
fs.closeSync(fileDescriptor);
console.log('ファイルハンドルを閉じました');
} catch (closeError) {
console.error(
'ファイルクローズエラー:',
closeError.message
);
}
}
}
}
// データベース接続の安全な管理(擬似実装)
static processDatabase(query) {
let connection = null;
let transaction = null;
try {
console.log('データベース接続を開始');
connection = this.connectToDatabase();
console.log('トランザクション開始');
transaction = connection.beginTransaction();
// データベース操作
const result = connection.execute(query);
// トランザクションコミット
transaction.commit();
console.log('トランザクションをコミットしました');
return result;
} catch (error) {
console.error(
'データベース処理エラー:',
error.message
);
// トランザクションのロールバック
if (transaction) {
try {
transaction.rollback();
console.log(
'トランザクションをロールバックしました'
);
} catch (rollbackError) {
console.error(
'ロールバックエラー:',
rollbackError.message
);
}
}
throw new Error(
`Database operation failed: ${error.message}`
);
} finally {
// リソースの段階的クリーンアップ
if (transaction) {
try {
transaction.close();
console.log('トランザクションを閉じました');
} catch (closeError) {
console.error(
'トランザクションクローズエラー:',
closeError.message
);
}
}
if (connection) {
try {
connection.close();
console.log('データベース接続を閉じました');
} catch (closeError) {
console.error(
'接続クローズエラー:',
closeError.message
);
}
}
}
}
// 擬似データベース接続クラス
static connectToDatabase() {
return {
beginTransaction: () => ({
commit: () => console.log('Transaction committed'),
rollback: () =>
console.log('Transaction rolled back'),
close: () => console.log('Transaction closed'),
}),
execute: (query) => ({ result: 'success', query }),
close: () => console.log('Connection closed'),
};
}
}
// 使用例
try {
const bytesProcessed =
ResourceManager.processFileWithResource(
'./package.json'
);
console.log(`処理完了: ${bytesProcessed} bytes`);
} catch (error) {
console.error('ファイル処理失敗:', error.message);
}
非同期エラーハンドリング:Promise・async/await での対応
Node.js における非同期エラーハンドリングは、同期処理とは根本的に異なるアプローチが必要です。Promise ベースの処理と async/await の適切な組み合わせにより、堅牢な非同期エラー制御を実現します。
Promise チェーンでのエラーハンドリング
Promise チェーンにおけるエラーの伝播と、各段階でのエラー処理方法を詳しく解説します。
javascriptconst fs = require('fs').promises;
const path = require('path');
class AsyncFileProcessor {
// Promise チェーンでの段階的エラーハンドリング
static processFileChain(filePath) {
return fs
.readFile(filePath, 'utf8')
.then((content) => {
console.log(`ファイル読み込み成功: ${filePath}`);
// JSON 解析
try {
return JSON.parse(content);
} catch (parseError) {
throw new Error(
`JSON parse error in ${filePath}: ${parseError.message}`
);
}
})
.then((data) => {
console.log('JSON 解析成功');
// データ検証
if (!data || typeof data !== 'object') {
throw new Error(
'Invalid data format: expected object'
);
}
if (!data.name) {
throw new Error('Missing required field: name');
}
return data;
})
.then((validatedData) => {
console.log('データ検証成功');
// データ変換
return {
...validatedData,
processed: true,
timestamp: new Date().toISOString(),
filePath: path.resolve(filePath),
};
})
.catch((error) => {
// Promise チェーン全体のエラーハンドリング
console.error(
'Promise チェーンエラー:',
error.message
);
// エラータイプ別の詳細処理
if (error.code === 'ENOENT') {
throw new Error(`File not found: ${filePath}`);
} else if (error.code === 'EACCES') {
throw new Error(`Permission denied: ${filePath}`);
} else if (
error.message.includes('JSON parse error')
) {
throw new Error(
`Invalid JSON format in file: ${filePath}`
);
} else if (
error.message.includes('Missing required field')
) {
throw new Error(
`Data validation failed for file: ${filePath} - ${error.message}`
);
} else {
throw new Error(
`File processing failed: ${error.message}`
);
}
});
}
// 複数の Promise の並列処理でのエラーハンドリング
static async processBatchFilesParallel(filePaths) {
console.log(
`並列処理開始: ${filePaths.length} ファイル`
);
// Promise.allSettled を使用して部分的な失敗を許容
const results = await Promise.allSettled(
filePaths.map((filePath) =>
this.processFileChain(filePath)
)
);
const successful = [];
const failed = [];
results.forEach((result, index) => {
const filePath = filePaths[index];
if (result.status === 'fulfilled') {
successful.push({
filePath,
data: result.value,
status: 'success',
});
} else {
failed.push({
filePath,
error: result.reason.message,
status: 'failed',
});
}
});
console.log(
`並列処理完了: 成功 ${successful.length}件, 失敗 ${failed.length}件`
);
return {
total: filePaths.length,
successful: successful.length,
failed: failed.length,
results: successful,
errors: failed,
};
}
// Promise.race での最初の成功/最初の失敗処理
static async processFileWithFallback(
primaryPath,
fallbackPaths
) {
const allPaths = [primaryPath, ...fallbackPaths];
console.log(
`フォールバック付きファイル処理: ${allPaths.length} パス`
);
try {
// 最初に成功したファイルの結果を返す
const result = await Promise.race(
allPaths.map(async (filePath, index) => {
try {
const data = await this.processFileChain(
filePath
);
console.log(
`成功したパス (${index}): ${filePath}`
);
return { data, filePath, index };
} catch (error) {
console.log(
`失敗したパス (${index}): ${filePath} - ${error.message}`
);
throw error;
}
})
);
return result;
} catch (error) {
// すべてのパスが失敗した場合
console.error(
'すべてのフォールバックパスが失敗しました'
);
throw new Error(
`All file paths failed. Last error: ${error.message}`
);
}
}
}
// Promise チェーンの使用例
async function demonstratePromiseChain() {
try {
const result =
await AsyncFileProcessor.processFileChain(
'./package.json'
);
console.log('処理結果:', result);
} catch (error) {
console.error('ファイル処理エラー:', error.message);
}
}
// 並列処理の使用例
async function demonstrateParallelProcessing() {
const files = [
'./package.json',
'./tsconfig.json',
'./nonexistent.json',
'./README.md',
];
try {
const batchResult =
await AsyncFileProcessor.processBatchFilesParallel(
files
);
console.log('\n=== 並列処理結果 ===');
console.log(`処理対象: ${batchResult.total}件`);
console.log(`成功: ${batchResult.successful}件`);
console.log(`失敗: ${batchResult.failed}件`);
if (batchResult.errors.length > 0) {
console.log('\n=== エラー詳細 ===');
batchResult.errors.forEach((error) => {
console.error(
`❌ ${error.filePath}: ${error.error}`
);
});
}
} catch (error) {
console.error('並列処理で致命的エラー:', error.message);
}
}
// フォールバック処理の使用例
async function demonstrateFallbackProcessing() {
const primaryFile = './config.production.json';
const fallbackFiles = [
'./config.staging.json',
'./config.default.json',
];
try {
const result =
await AsyncFileProcessor.processFileWithFallback(
primaryFile,
fallbackFiles
);
console.log('フォールバック処理成功:', result.filePath);
} catch (error) {
console.error('フォールバック処理失敗:', error.message);
}
}
async/await での包括的エラーハンドリング
async/await 構文を使用した、より読みやすく保守性の高いエラーハンドリングパターンを実装します。
javascriptconst fs = require('fs').promises;
const https = require('https');
const { promisify } = require('util');
class AsyncAwaitErrorHandler {
// 複層的な async/await エラーハンドリング
static async downloadAndProcessFile(url, outputPath) {
let tempFile = null;
try {
console.log(`ダウンロード開始: ${url}`);
// HTTP ダウンロード(async/await でラップ)
const downloadedData = await this.downloadFile(url);
console.log(
`ダウンロード完了: ${downloadedData.length} bytes`
);
// 一時ファイルの作成
tempFile = `${outputPath}.tmp`;
await fs.writeFile(tempFile, downloadedData);
console.log(`一時ファイル作成: ${tempFile}`);
// ファイル内容の検証
await this.validateFileContent(tempFile);
console.log('ファイル内容の検証完了');
// 最終ファイルへのリネーム(アトミック操作)
await fs.rename(tempFile, outputPath);
console.log(`ファイル保存完了: ${outputPath}`);
tempFile = null; // 成功時は一時ファイルはクリーンアップ不要
return {
success: true,
outputPath,
size: downloadedData.length,
timestamp: new Date().toISOString(),
};
} catch (error) {
console.error(
'ダウンロード・処理エラー:',
error.message
);
// エラータイプ別の詳細処理
if (error.code === 'ENOTFOUND') {
throw new Error(
`DNS resolution failed for URL: ${url}`
);
} else if (error.code === 'ECONNREFUSED') {
throw new Error(
`Connection refused to URL: ${url}`
);
} else if (error.code === 'ETIMEDOUT') {
throw new Error(`Request timeout for URL: ${url}`);
} else if (error.message.includes('HTTP')) {
throw new Error(
`HTTP error while downloading: ${error.message}`
);
} else if (error.code === 'ENOSPC') {
throw new Error(
`Insufficient disk space for file: ${outputPath}`
);
} else if (error.message.includes('validation')) {
throw new Error(
`File validation failed: ${error.message}`
);
} else {
throw new Error(
`Download and processing failed: ${error.message}`
);
}
} finally {
// 一時ファイルのクリーンアップ
if (tempFile) {
try {
await fs.unlink(tempFile);
console.log(`一時ファイル削除: ${tempFile}`);
} catch (cleanupError) {
console.error(
`一時ファイル削除エラー: ${cleanupError.message}`
);
}
}
}
}
// HTTP ダウンロードのPromise化
static downloadFile(url) {
return new Promise((resolve, reject) => {
const chunks = [];
const request = https.get(url, (response) => {
// HTTP ステータスコードのチェック
if (response.statusCode !== 200) {
reject(
new Error(
`HTTP ${response.statusCode}: ${response.statusMessage}`
)
);
return;
}
response.on('data', (chunk) => {
chunks.push(chunk);
});
response.on('end', () => {
const data = Buffer.concat(chunks);
resolve(data);
});
response.on('error', (error) => {
reject(
new Error(`Response error: ${error.message}`)
);
});
});
request.on('error', (error) => {
reject(
new Error(`Request error: ${error.message}`)
);
});
// タイムアウト設定
request.setTimeout(30000, () => {
request.abort();
reject(
new Error('Request timeout after 30 seconds')
);
});
});
}
// ファイル内容の検証
static async validateFileContent(filePath) {
try {
const stats = await fs.stat(filePath);
// ファイルサイズの検証
if (stats.size === 0) {
throw new Error('File is empty');
}
if (stats.size > 100 * 1024 * 1024) {
// 100MB制限
throw new Error(
`File too large: ${stats.size} bytes`
);
}
// ファイル内容の基本検証(テキストファイルの場合)
const content = await fs.readFile(filePath, 'utf8');
// BOM チェック
if (content.charCodeAt(0) === 0xfeff) {
console.log('BOM detected and will be handled');
}
// 基本的な文字エンコーディング検証
if (content.includes('\uFFFD')) {
throw new Error(
'Invalid character encoding detected'
);
}
console.log(`ファイル検証完了: ${stats.size} bytes`);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(
`Validation failed - file not found: ${filePath}`
);
} else if (error.message.includes('validation')) {
throw error; // カスタム検証エラーはそのまま
} else {
throw new Error(
`File validation error: ${error.message}`
);
}
}
}
// 複数の非同期操作の逐次実行
static async processFilesSequentially(operations) {
const results = [];
const errors = [];
for (let i = 0; i < operations.length; i++) {
const operation = operations[i];
try {
console.log(
`操作 ${i + 1}/${operations.length} 実行中: ${
operation.description
}`
);
const startTime = Date.now();
const result = await operation.execute();
const duration = Date.now() - startTime;
results.push({
index: i,
description: operation.description,
result,
duration,
status: 'success',
});
console.log(`操作 ${i + 1} 完了 (${duration}ms)`);
} catch (error) {
console.error(
`操作 ${i + 1} 失敗: ${error.message}`
);
errors.push({
index: i,
description: operation.description,
error: error.message,
status: 'failed',
});
// 操作失敗時の継続可否判定
if (operation.critical) {
console.error(
'重要な操作が失敗しました。処理を中断します。'
);
throw new Error(
`Critical operation failed: ${error.message}`
);
}
console.log(
'非重要な操作の失敗。処理を継続します。'
);
}
}
return {
completed: results.length,
failed: errors.length,
results,
errors,
};
}
}
// 使用例: ダウンロードと処理
async function demonstrateDownloadProcessing() {
try {
const result =
await AsyncAwaitErrorHandler.downloadAndProcessFile(
'https://api.github.com/repos/nodejs/node/releases/latest',
'./latest-release.json'
);
console.log('ダウンロード処理成功:', result);
} catch (error) {
console.error('ダウンロード処理失敗:', error.message);
}
}
// 使用例: 逐次処理
async function demonstrateSequentialProcessing() {
const operations = [
{
description: 'ディレクトリ作成',
critical: true,
execute: async () => {
await fs.mkdir('./temp', { recursive: true });
return { created: './temp' };
},
},
{
description: 'ファイル1の作成',
critical: false,
execute: async () => {
await fs.writeFile('./temp/file1.txt', 'content1');
return { created: './temp/file1.txt' };
},
},
{
description: 'ファイル2の作成(失敗想定)',
critical: false,
execute: async () => {
throw new Error('Simulated failure');
},
},
{
description: 'ファイル3の作成',
critical: true,
execute: async () => {
await fs.writeFile('./temp/file3.txt', 'content3');
return { created: './temp/file3.txt' };
},
},
];
try {
const result =
await AsyncAwaitErrorHandler.processFilesSequentially(
operations
);
console.log('逐次処理結果:', result);
} catch (error) {
console.error('逐次処理で致命的エラー:', error.message);
}
}
イベント駆動エラー処理:EventEmitter とエラーイベント
Node.js のイベント駆動アーキテクチャにおいて、EventEmitter はエラーハンドリングの重要な役割を担います。適切なエラーイベントの実装により、アプリケーションの堅牢性と可観測性が大幅に向上します。
EventEmitter の基本的なエラーハンドリング
EventEmitter におけるエラーイベントの基本的な実装パターンから、実践的な応用まで詳しく解説します。
javascriptconst { EventEmitter } = require('events');
const fs = require('fs');
class FileWatcher extends EventEmitter {
constructor(filePath) {
super();
this.filePath = filePath;
this.watcher = null;
this.retryCount = 0;
this.maxRetries = 3;
// エラーハンドラーの設定
this.setupErrorHandling();
}
setupErrorHandling() {
// uncaughtException を防ぐためのデフォルトエラーハンドラー
this.on('error', (error) => {
console.error(
`FileWatcher エラー [${this.filePath}]:`,
error.message
);
// エラータイプ別の自動回復処理
if (
error.code === 'ENOENT' &&
this.retryCount < this.maxRetries
) {
console.log(
`ファイル復旧を試行中... (${
this.retryCount + 1
}/${this.maxRetries})`
);
this.retryCount++;
setTimeout(() => this.startWatching(), 5000);
} else {
this.emit('watchError', {
filePath: this.filePath,
error: error.message,
code: error.code,
retryCount: this.retryCount,
timestamp: new Date().toISOString(),
});
}
});
// プロセス終了時のクリーンアップ
process.on('SIGINT', () => this.stop());
process.on('SIGTERM', () => this.stop());
}
startWatching() {
try {
// 既存のウォッチャーをクリーンアップ
if (this.watcher) {
this.watcher.close();
}
console.log(`ファイル監視開始: ${this.filePath}`);
this.watcher = fs.watch(
this.filePath,
{ persistent: true },
(eventType, filename) => {
try {
this.handleFileChange(eventType, filename);
} catch (error) {
this.emit(
'error',
new Error(
`File change handling error: ${error.message}`
)
);
}
}
);
// ウォッチャー自体のエラーハンドリング
this.watcher.on('error', (error) => {
console.error('fs.watch エラー:', error.message);
// 詳細なエラー分析
let errorDetails = {
code: error.code,
message: error.message,
filePath: this.filePath,
timestamp: new Date().toISOString(),
};
switch (error.code) {
case 'ENOENT':
errorDetails.description =
'ファイルまたはディレクトリが存在しません';
break;
case 'EACCES':
errorDetails.description =
'ファイルへのアクセス権限がありません';
break;
case 'EMFILE':
errorDetails.description =
'ファイル記述子の上限に達しました';
break;
case 'ENOTDIR':
errorDetails.description =
'パスの一部がディレクトリではありません';
break;
default:
errorDetails.description =
'予期しないファイルシステムエラー';
}
this.emit(
'error',
Object.assign(error, errorDetails)
);
});
this.emit('watchStart', {
filePath: this.filePath,
timestamp: new Date().toISOString(),
});
// 監視開始の成功をリセット
this.retryCount = 0;
} catch (error) {
console.error(
'ファイル監視開始エラー:',
error.message
);
this.emit(
'error',
new Error(
`Failed to start watching: ${error.message}`
)
);
}
}
handleFileChange(eventType, filename) {
console.log(
`ファイル変更検出: ${eventType} - ${
filename || this.filePath
}`
);
const changeInfo = {
eventType,
filename: filename || this.filePath,
filePath: this.filePath,
timestamp: new Date().toISOString(),
};
// 変更タイプ別の処理
switch (eventType) {
case 'change':
this.emit('fileChanged', changeInfo);
break;
case 'rename':
this.emit('fileRenamed', changeInfo);
// リネーム後のファイル存在確認
this.verifyFileExists();
break;
default:
this.emit('fileEvent', changeInfo);
}
}
async verifyFileExists() {
try {
await fs.promises.access(
this.filePath,
fs.constants.F_OK
);
this.emit('fileVerified', {
filePath: this.filePath,
exists: true,
});
} catch (error) {
this.emit('fileVerified', {
filePath: this.filePath,
exists: false,
});
this.emit(
'error',
new Error(`File no longer exists: ${this.filePath}`)
);
}
}
stop() {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
console.log(`ファイル監視停止: ${this.filePath}`);
this.emit('watchStop', {
filePath: this.filePath,
timestamp: new Date().toISOString(),
});
}
}
}
// 使用例
function demonstrateFileWatcher() {
const watcher = new FileWatcher('./package.json');
// 各種イベントのハンドラー設定
watcher.on('watchStart', (info) => {
console.log('✅ 監視開始:', info.filePath);
});
watcher.on('fileChanged', (info) => {
console.log('📝 ファイル変更:', info);
});
watcher.on('fileRenamed', (info) => {
console.log('📄 ファイルリネーム:', info);
});
watcher.on('fileVerified', (info) => {
console.log(
'🔍 ファイル確認:',
`${info.filePath} - ${info.exists ? '存在' : '削除'}`
);
});
watcher.on('watchError', (errorInfo) => {
console.error('❌ 監視エラー:', errorInfo);
});
watcher.on('watchStop', (info) => {
console.log('⏹️ 監視停止:', info.filePath);
});
// 監視開始
watcher.startWatching();
// 10秒後に停止(デモ用)
setTimeout(() => {
watcher.stop();
}, 10000);
}
ストリーム処理でのイベント駆動エラーハンドリング
Node.js ストリームにおける包括的なエラーハンドリングパターンを実装します。
javascriptconst { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const fs = require('fs');
const zlib = require('zlib');
const pipelineAsync = promisify(pipeline);
class DataTransformStream extends Transform {
constructor(options = {}) {
super({ objectMode: true });
this.processedCount = 0;
this.errorCount = 0;
this.options = {
skipInvalidRecords:
options.skipInvalidRecords || false,
maxErrors: options.maxErrors || 10,
...options,
};
this.setupErrorHandling();
}
setupErrorHandling() {
// ストリームエラーの詳細ログ
this.on('error', (error) => {
console.error('DataTransformStream エラー:', {
message: error.message,
processedCount: this.processedCount,
errorCount: this.errorCount,
timestamp: new Date().toISOString(),
});
});
// ストリーム終了時の統計情報
this.on('end', () => {
this.emit('transformComplete', {
processed: this.processedCount,
errors: this.errorCount,
successRate: (
(this.processedCount /
(this.processedCount + this.errorCount)) *
100
).toFixed(2),
});
});
}
_transform(chunk, encoding, callback) {
try {
// データの解析と変換
const data = this.parseData(chunk);
const transformedData = this.transformData(data);
this.processedCount++;
// 処理進捗の通知
if (this.processedCount % 100 === 0) {
this.emit('progress', {
processed: this.processedCount,
errors: this.errorCount,
timestamp: new Date().toISOString(),
});
}
callback(null, transformedData);
} catch (error) {
this.errorCount++;
const errorInfo = {
message: error.message,
chunk: chunk.toString().substring(0, 100) + '...', // プレビュー
processedCount: this.processedCount,
errorCount: this.errorCount,
timestamp: new Date().toISOString(),
};
console.error('変換エラー:', errorInfo);
this.emit('transformError', errorInfo);
// エラー処理ポリシーの適用
if (this.options.skipInvalidRecords) {
// 無効なレコードをスキップして処理継続
console.log(
'無効なレコードをスキップして処理を継続します'
);
callback();
} else if (
this.errorCount >= this.options.maxErrors
) {
// エラー数の上限に達した場合は処理を停止
const fatalError = new Error(
`Too many errors: ${this.errorCount}/${this.options.maxErrors}`
);
fatalError.code = 'ERR_TOO_MANY_ERRORS';
callback(fatalError);
} else {
// エラーをそのまま伝播
callback(error);
}
}
}
parseData(chunk) {
try {
const dataString = chunk.toString().trim();
if (!dataString) {
throw new Error('Empty data chunk');
}
// JSON データの解析
if (
dataString.startsWith('{') ||
dataString.startsWith('[')
) {
return JSON.parse(dataString);
}
// CSV データの簡易解析
if (dataString.includes(',')) {
const values = dataString
.split(',')
.map((v) => v.trim());
return { type: 'csv', values };
}
// プレーンテキスト
return { type: 'text', content: dataString };
} catch (error) {
throw new Error(
`Data parsing failed: ${error.message}`
);
}
}
transformData(data) {
try {
// データタイプ別の変換処理
switch (data.type) {
case 'csv':
return {
...data,
processed: true,
rowCount: data.values.length,
timestamp: new Date().toISOString(),
};
case 'text':
return {
...data,
processed: true,
wordCount: data.content.split(/\s+/).length,
timestamp: new Date().toISOString(),
};
default:
// JSON オブジェクト
return {
...data,
processed: true,
fieldCount: Object.keys(data).length,
timestamp: new Date().toISOString(),
};
}
} catch (error) {
throw new Error(
`Data transformation failed: ${error.message}`
);
}
}
}
class StreamProcessor extends EventEmitter {
constructor() {
super();
this.activeStreams = new Set();
}
async processFileWithStreams(
inputPath,
outputPath,
options = {}
) {
let inputStream = null;
let outputStream = null;
let transformStream = null;
let compressionStream = null;
try {
console.log(
`ストリーム処理開始: ${inputPath} -> ${outputPath}`
);
// ストリームの作成
inputStream = fs.createReadStream(inputPath);
outputStream = fs.createWriteStream(outputPath);
transformStream = new DataTransformStream(options);
compressionStream = zlib.createGzip();
// ストリームの登録
[
inputStream,
outputStream,
transformStream,
compressionStream,
].forEach((stream) => {
this.activeStreams.add(stream);
});
// 各ストリームのエラーハンドリング
this.setupStreamErrorHandlers({
input: inputStream,
output: outputStream,
transform: transformStream,
compression: compressionStream,
});
// 変換ストリームのイベント監視
transformStream.on('progress', (info) => {
console.log(
`処理進捗: ${info.processed}件処理, ${info.errors}件エラー`
);
this.emit('progress', info);
});
transformStream.on('transformError', (errorInfo) => {
console.error('変換エラー発生:', errorInfo);
this.emit('streamError', errorInfo);
});
transformStream.on('transformComplete', (stats) => {
console.log('変換完了:', stats);
this.emit('streamComplete', stats);
});
// パイプラインの実行
await pipelineAsync(
inputStream,
transformStream,
compressionStream,
outputStream
);
console.log(`ストリーム処理完了: ${outputPath}`);
return {
success: true,
inputPath,
outputPath,
timestamp: new Date().toISOString(),
};
} catch (error) {
console.error('ストリーム処理エラー:', error.message);
// エラータイプ別の詳細情報
let errorDetails = {
inputPath,
outputPath,
error: error.message,
code: error.code,
timestamp: new Date().toISOString(),
};
if (error.code === 'ENOENT') {
errorDetails.description =
'入力ファイルが見つかりません';
} else if (error.code === 'EACCES') {
errorDetails.description =
'ファイルアクセス権限エラー';
} else if (error.code === 'ENOSPC') {
errorDetails.description = 'ディスク容量不足';
} else if (error.code === 'ERR_TOO_MANY_ERRORS') {
errorDetails.description =
'変換エラーが上限に達しました';
} else {
errorDetails.description =
'ストリーム処理中の予期しないエラー';
}
this.emit('streamFailed', errorDetails);
throw new Error(
`Stream processing failed: ${errorDetails.description}`
);
} finally {
// ストリームのクリーンアップ
this.cleanup();
}
}
setupStreamErrorHandlers(streams) {
Object.entries(streams).forEach(([name, stream]) => {
stream.on('error', (error) => {
console.error(
`${name} ストリームエラー:`,
error.message
);
this.emit('streamError', {
streamType: name,
error: error.message,
code: error.code,
timestamp: new Date().toISOString(),
});
});
});
}
cleanup() {
this.activeStreams.forEach((stream) => {
try {
if (
stream &&
typeof stream.destroy === 'function'
) {
stream.destroy();
}
} catch (error) {
console.error(
'ストリームクリーンアップエラー:',
error.message
);
}
});
this.activeStreams.clear();
console.log('ストリームクリーンアップ完了');
}
}
// 使用例
async function demonstrateStreamProcessing() {
const processor = new StreamProcessor();
// イベントハンドラーの設定
processor.on('progress', (info) => {
console.log(`📊 進捗: ${info.processed}件処理済み`);
});
processor.on('streamError', (errorInfo) => {
console.error(
`❌ ストリームエラー: ${errorInfo.streamType} - ${errorInfo.error}`
);
});
processor.on('streamComplete', (stats) => {
console.log(
`✅ 処理完了: 成功率 ${stats.successRate}%`
);
});
processor.on('streamFailed', (errorDetails) => {
console.error(
`💥 処理失敗: ${errorDetails.description}`
);
});
try {
const result = await processor.processFileWithStreams(
'./package.json',
'./output.json.gz',
{
skipInvalidRecords: true,
maxErrors: 5,
}
);
console.log('ストリーム処理結果:', result);
} catch (error) {
console.error('ストリーム処理失敗:', error.message);
}
}
グローバルエラーハンドラー:uncaughtException と unhandledRejection
Node.js アプリケーションの最後の砦となるグローバルエラーハンドラーの実装は、アプリケーションの安定性と可観測性において極めて重要です。適切な実装により、予期しないエラーによるプロセスクラッシュを防ぎ、エラー情報の収集と分析が可能になります。
uncaughtException ハンドラーの実装
キャッチされなかった例外を適切に処理し、アプリケーションの安全なシャットダウンを実現します。
javascriptconst fs = require('fs').promises;
const path = require('path');
const { EventEmitter } = require('events');
class GlobalErrorHandler extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
logDirectory: options.logDirectory || './logs',
gracefulShutdownTimeout:
options.gracefulShutdownTimeout || 10000,
enableCrashReports:
options.enableCrashReports !== false,
maxLogFiles: options.maxLogFiles || 50,
...options,
};
this.isShuttingDown = false;
this.activeConnections = new Set();
this.setupGlobalHandlers();
}
async setupGlobalHandlers() {
// ログディレクトリの作成
try {
await fs.mkdir(this.options.logDirectory, {
recursive: true,
});
} catch (error) {
console.error(
'ログディレクトリ作成エラー:',
error.message
);
}
// uncaughtException ハンドラー
process.on(
'uncaughtException',
async (error, origin) => {
const errorInfo = {
type: 'uncaughtException',
message: error.message,
stack: error.stack,
origin,
code: error.code,
timestamp: new Date().toISOString(),
pid: process.pid,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
};
console.error(
'🚨 キャッチされていない例外が発生しました:',
errorInfo
);
try {
// エラーログの保存
await this.logCriticalError(errorInfo);
// エラー通知イベントの発行
this.emit('criticalError', errorInfo);
// 外部監視システムへの通知(例:APM、Slack等)
await this.notifyExternalSystems(errorInfo);
} catch (logError) {
console.error(
'エラーログ記録失敗:',
logError.message
);
}
// 安全なシャットダウンの実行
await this.gracefulShutdown(
'uncaughtException',
errorInfo
);
}
);
// unhandledRejection ハンドラー
process.on(
'unhandledRejection',
async (reason, promise) => {
const errorInfo = {
type: 'unhandledRejection',
reason:
reason instanceof Error
? {
message: reason.message,
stack: reason.stack,
code: reason.code,
}
: reason,
promise: promise.toString(),
timestamp: new Date().toISOString(),
pid: process.pid,
memoryUsage: process.memoryUsage(),
};
console.error(
'🚨 処理されていない Promise の拒否が発生しました:',
errorInfo
);
try {
await this.logCriticalError(errorInfo);
this.emit('criticalError', errorInfo);
await this.notifyExternalSystems(errorInfo);
// unhandledRejection の場合は即座にシャットダウンしない
// ただし、頻発する場合は危険な状態と判断
await this.handleUnhandledRejection(errorInfo);
} catch (logError) {
console.error(
'エラーログ記録失敗:',
logError.message
);
}
}
);
// プロセス終了シグナルのハンドリング
['SIGINT', 'SIGTERM'].forEach((signal) => {
process.on(signal, async () => {
console.log(
`🛑 ${signal} シグナルを受信しました。グレースフルシャットダウンを開始します...`
);
await this.gracefulShutdown(signal);
});
});
// プロセス警告のハンドリング
process.on('warning', (warning) => {
const warningInfo = {
type: 'processWarning',
name: warning.name,
message: warning.message,
stack: warning.stack,
timestamp: new Date().toISOString(),
};
console.warn('⚠️ プロセス警告:', warningInfo);
this.emit('processWarning', warningInfo);
});
}
async logCriticalError(errorInfo) {
try {
const logFileName = `error-${
new Date().toISOString().split('T')[0]
}.log`;
const logFilePath = path.join(
this.options.logDirectory,
logFileName
);
const logEntry = {
...errorInfo,
environment: {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
argv: process.argv,
env: {
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT,
},
},
};
const logLine =
JSON.stringify(logEntry, null, 2) +
'\n\n' +
'='.repeat(80) +
'\n\n';
await fs.appendFile(logFilePath, logLine);
console.log(
`エラーログを保存しました: ${logFilePath}`
);
// ログファイルローテーション
await this.rotateLogFiles();
} catch (error) {
console.error('エラーログ保存失敗:', error.message);
}
}
async rotateLogFiles() {
try {
const files = await fs.readdir(
this.options.logDirectory
);
const errorLogFiles = files
.filter(
(file) =>
file.startsWith('error-') &&
file.endsWith('.log')
)
.sort()
.reverse();
if (errorLogFiles.length > this.options.maxLogFiles) {
const filesToDelete = errorLogFiles.slice(
this.options.maxLogFiles
);
for (const file of filesToDelete) {
const filePath = path.join(
this.options.logDirectory,
file
);
await fs.unlink(filePath);
console.log(`古いログファイルを削除: ${file}`);
}
}
} catch (error) {
console.error(
'ログローテーションエラー:',
error.message
);
}
}
async notifyExternalSystems(errorInfo) {
try {
// 外部監視システムへの通知(実装例)
if (this.options.webhookUrl) {
const payload = {
text: `🚨 Critical Error in ${
process.env.NODE_ENV || 'unknown'
} environment`,
attachments: [
{
color: 'danger',
fields: [
{
title: 'Error Type',
value: errorInfo.type,
short: true,
},
{
title: 'Message',
value: errorInfo.message,
short: false,
},
{
title: 'PID',
value: errorInfo.pid,
short: true,
},
{
title: 'Timestamp',
value: errorInfo.timestamp,
short: true,
},
],
},
],
};
// webhook 送信の実装(例)
console.log('外部システムに通知予定:', payload);
}
// APM ツールへの送信(例:New Relic、DataDog等)
if (this.options.apmEnabled) {
console.log('APM ツールにエラー情報を送信予定');
}
} catch (error) {
console.error(
'外部システム通知エラー:',
error.message
);
}
}
async handleUnhandledRejection(errorInfo) {
// unhandledRejection の頻度を監視
const recentRejections = this.getRecentRejections();
recentRejections.push(errorInfo.timestamp);
// 直近5分間で5回以上の unhandledRejection が発生した場合
const fiveMinutesAgo = new Date(
Date.now() - 5 * 60 * 1000
).toISOString();
const recentCount = recentRejections.filter(
(timestamp) => timestamp > fiveMinutesAgo
).length;
if (recentCount >= 5) {
console.error(
'🚨 unhandledRejection が頻発しています。システムが不安定な可能性があります。'
);
const criticalInfo = {
...errorInfo,
type: 'criticalUnhandledRejection',
recentCount,
message:
'Multiple unhandled rejections detected - system may be unstable',
};
await this.gracefulShutdown(
'criticalUnhandledRejection',
criticalInfo
);
}
}
getRecentRejections() {
if (!this._recentRejections) {
this._recentRejections = [];
}
return this._recentRejections;
}
async gracefulShutdown(reason, errorInfo = null) {
if (this.isShuttingDown) {
console.log('シャットダウンが既に進行中です...');
return;
}
this.isShuttingDown = true;
console.log(
`🚪 グレースフルシャットダウン開始 (理由: ${reason})`
);
const shutdownInfo = {
reason,
errorInfo,
timestamp: new Date().toISOString(),
activeConnections: this.activeConnections.size,
};
this.emit('shutdownStart', shutdownInfo);
try {
// アクティブな接続の終了を待機
await this.closeActiveConnections();
// 重要なリソースのクリーンアップ
await this.cleanupResources();
// 最終ログの記録
await this.logShutdown(shutdownInfo);
console.log('✅ グレースフルシャットダウン完了');
this.emit('shutdownComplete', shutdownInfo);
} catch (shutdownError) {
console.error(
'シャットダウンエラー:',
shutdownError.message
);
}
// 強制終了のタイムアウト
setTimeout(() => {
console.error(
'⏰ グレースフルシャットダウンがタイムアウトしました。強制終了します。'
);
process.exit(1);
}, this.options.gracefulShutdownTimeout);
// 通常の終了
process.exit(reason === 'uncaughtException' ? 1 : 0);
}
async closeActiveConnections() {
console.log(
`🔗 アクティブな接続を終了中... (${this.activeConnections.size}件)`
);
const closePromises = Array.from(
this.activeConnections
).map(async (connection) => {
try {
if (connection.close) {
await connection.close();
} else if (connection.end) {
connection.end();
}
} catch (error) {
console.error('接続終了エラー:', error.message);
}
});
await Promise.allSettled(closePromises);
this.activeConnections.clear();
console.log('接続終了完了');
}
async cleanupResources() {
console.log('🧹 リソースクリーンアップ中...');
try {
// データベース接続の終了
// キャッシュのクリア
// 一時ファイルの削除
// 外部サービスとの接続終了
console.log('リソースクリーンアップ完了');
} catch (error) {
console.error(
'リソースクリーンアップエラー:',
error.message
);
}
}
async logShutdown(shutdownInfo) {
try {
const shutdownLogPath = path.join(
this.options.logDirectory,
'shutdown.log'
);
const logEntry =
JSON.stringify(shutdownInfo, null, 2) + '\n\n';
await fs.appendFile(shutdownLogPath, logEntry);
} catch (error) {
console.error(
'シャットダウンログ記録エラー:',
error.message
);
}
}
// 接続の追加(HTTPサーバー、データベース等)
addConnection(connection) {
this.activeConnections.add(connection);
}
// 接続の削除
removeConnection(connection) {
this.activeConnections.delete(connection);
}
}
// 使用例
function setupGlobalErrorHandling() {
const errorHandler = new GlobalErrorHandler({
logDirectory: './logs',
gracefulShutdownTimeout: 15000,
webhookUrl: process.env.ERROR_WEBHOOK_URL,
apmEnabled: process.env.NODE_ENV === 'production',
});
// イベントハンドラーの設定
errorHandler.on('criticalError', (errorInfo) => {
console.log(
'📧 クリティカルエラー通知送信:',
errorInfo.type
);
});
errorHandler.on('processWarning', (warningInfo) => {
console.log('⚠️ プロセス警告記録:', warningInfo.name);
});
errorHandler.on('shutdownStart', (shutdownInfo) => {
console.log('🚪 シャットダウン開始通知');
});
errorHandler.on('shutdownComplete', (shutdownInfo) => {
console.log('✅ シャットダウン完了通知');
});
return errorHandler;
}
// テスト用の関数(実際のアプリケーションでは使用しないでください)
function triggerTestErrors() {
console.log(
'⚠️ テスト用エラーを発生させています(実際のアプリケーションでは削除してください)'
);
// uncaughtException のテスト
setTimeout(() => {
throw new Error('Test uncaught exception');
}, 5000);
// unhandledRejection のテスト
setTimeout(() => {
Promise.reject(new Error('Test unhandled rejection'));
}, 7000);
}
カスタムエラークラス設計:意味のあるエラー分類
効果的なエラーハンドリングには、エラーの種類と原因を明確に分類できるカスタムエラークラスの設計が不可欠です。適切なエラー分類により、デバッグ効率の向上と運用時のトラブルシューティングが大幅に改善されます。
階層的エラークラス設計
アプリケーションドメインに応じた階層的なエラークラス構造を実装します。
javascript// ベースエラークラス
class ApplicationError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name;
this.timestamp = new Date().toISOString();
this.code = options.code || 'UNKNOWN_ERROR';
this.statusCode = options.statusCode || 500;
this.isOperational = options.isOperational !== false; // デフォルトは operational
this.details = options.details || {};
this.context = options.context || {};
// スタックトレースの調整
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
// エラーのシリアライゼーション対応
Object.defineProperty(this, 'isApplicationError', {
value: true,
writable: false,
enumerable: false,
});
}
// エラー情報のJSON表現
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
statusCode: this.statusCode,
timestamp: this.timestamp,
isOperational: this.isOperational,
details: this.details,
context: this.context,
stack: this.stack,
};
}
// ログ出力用の整形
toLogFormat() {
return {
level: this.isOperational ? 'error' : 'fatal',
timestamp: this.timestamp,
errorType: this.name,
message: this.message,
code: this.code,
statusCode: this.statusCode,
details: this.details,
context: this.context,
};
}
// エラーの重要度判定
getSeverity() {
if (!this.isOperational) return 'critical';
if (this.statusCode >= 500) return 'high';
if (this.statusCode >= 400) return 'medium';
return 'low';
}
}
// データ関連エラー
class DataError extends ApplicationError {
constructor(message, options = {}) {
super(message, {
code: 'DATA_ERROR',
statusCode: 400,
...options,
});
}
}
class ValidationError extends DataError {
constructor(
message,
validationDetails = {},
options = {}
) {
super(message, {
code: 'VALIDATION_ERROR',
statusCode: 400,
details: {
validationErrors: validationDetails,
...options.details,
},
...options,
});
this.validationDetails = validationDetails;
}
// 特定フィールドのエラー取得
getFieldErrors(fieldName) {
return this.validationDetails[fieldName] || [];
}
// すべてのフィールドエラーを整形
getAllFieldErrors() {
return Object.entries(this.validationDetails).map(
([field, errors]) => ({
field,
errors: Array.isArray(errors) ? errors : [errors],
})
);
}
}
class DatabaseError extends DataError {
constructor(message, dbDetails = {}, options = {}) {
super(message, {
code: 'DATABASE_ERROR',
statusCode: 500,
isOperational: true,
details: {
operation: dbDetails.operation,
table: dbDetails.table,
query: dbDetails.query,
constraint: dbDetails.constraint,
...options.details,
},
...options,
});
this.dbDetails = dbDetails;
}
// データベース固有のエラーコード判定
isDuplicateKeyError() {
return (
this.code === 'ER_DUP_ENTRY' || this.code === '23505'
);
}
isForeignKeyError() {
return (
this.code === 'ER_NO_REFERENCED_ROW' ||
this.code === '23503'
);
}
isConnectionError() {
return [
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
].includes(this.code);
}
}
// ネットワーク関連エラー
class NetworkError extends ApplicationError {
constructor(message, networkDetails = {}, options = {}) {
super(message, {
code: 'NETWORK_ERROR',
statusCode: 502,
isOperational: true,
details: {
url: networkDetails.url,
method: networkDetails.method,
timeout: networkDetails.timeout,
retryAttempt: networkDetails.retryAttempt,
...options.details,
},
...options,
});
this.networkDetails = networkDetails;
}
}
class HttpError extends NetworkError {
constructor(message, httpDetails = {}, options = {}) {
const statusCode = httpDetails.statusCode || 500;
super(message, httpDetails, {
code: `HTTP_${statusCode}`,
statusCode,
...options,
});
this.httpDetails = httpDetails;
}
// HTTPステータスコード分類
isClientError() {
return this.statusCode >= 400 && this.statusCode < 500;
}
isServerError() {
return this.statusCode >= 500;
}
isRetryable() {
// リトライ可能なHTTPエラーの判定
const retryableStatuses = [429, 502, 503, 504];
return retryableStatuses.includes(this.statusCode);
}
}
class ExternalServiceError extends NetworkError {
constructor(
serviceName,
message,
serviceDetails = {},
options = {}
) {
super(message, serviceDetails, {
code: 'EXTERNAL_SERVICE_ERROR',
statusCode: 502,
details: {
serviceName,
serviceEndpoint: serviceDetails.endpoint,
...serviceDetails,
...options.details,
},
...options,
});
this.serviceName = serviceName;
this.serviceDetails = serviceDetails;
}
}
// ビジネスロジック関連エラー
class BusinessLogicError extends ApplicationError {
constructor(message, businessContext = {}, options = {}) {
super(message, {
code: 'BUSINESS_LOGIC_ERROR',
statusCode: 400,
isOperational: true,
details: businessContext,
...options,
});
this.businessContext = businessContext;
}
}
class AuthorizationError extends BusinessLogicError {
constructor(message, authContext = {}, options = {}) {
super(message, authContext, {
code: 'AUTHORIZATION_ERROR',
statusCode: 403,
details: {
userId: authContext.userId,
requiredPermissions:
authContext.requiredPermissions,
actualPermissions: authContext.actualPermissions,
resource: authContext.resource,
action: authContext.action,
...options.details,
},
...options,
});
this.authContext = authContext;
}
}
class AuthenticationError extends BusinessLogicError {
constructor(message, authDetails = {}, options = {}) {
super(message, authDetails, {
code: 'AUTHENTICATION_ERROR',
statusCode: 401,
details: {
attemptedCredentials:
authDetails.username || authDetails.email,
ipAddress: authDetails.ipAddress,
userAgent: authDetails.userAgent,
...options.details,
},
...options,
});
}
}
class ResourceNotFoundError extends BusinessLogicError {
constructor(resourceType, resourceId, options = {}) {
const message = `${resourceType} with ID '${resourceId}' not found`;
super(
message,
{
resourceType,
resourceId,
},
{
code: 'RESOURCE_NOT_FOUND',
statusCode: 404,
...options,
}
);
this.resourceType = resourceType;
this.resourceId = resourceId;
}
}
// システム関連エラー
class SystemError extends ApplicationError {
constructor(message, systemDetails = {}, options = {}) {
super(message, systemDetails, {
code: 'SYSTEM_ERROR',
statusCode: 500,
isOperational: false, // システムエラーは通常非運用的
...options,
});
this.systemDetails = systemDetails;
}
}
class ConfigurationError extends SystemError {
constructor(message, configDetails = {}, options = {}) {
super(message, configDetails, {
code: 'CONFIGURATION_ERROR',
statusCode: 500,
details: {
configKey: configDetails.configKey,
expectedType: configDetails.expectedType,
actualValue: configDetails.actualValue,
...options.details,
},
...options,
});
}
}
class ResourceExhaustionError extends SystemError {
constructor(
resourceType,
currentUsage,
limit,
options = {}
) {
const message = `${resourceType} exhausted: ${currentUsage}/${limit}`;
super(
message,
{
resourceType,
currentUsage,
limit,
},
{
code: 'RESOURCE_EXHAUSTION',
statusCode: 503,
isOperational: true,
...options,
}
);
this.resourceType = resourceType;
this.currentUsage = currentUsage;
this.limit = limit;
}
}
// エラーファクトリークラス
class ErrorFactory {
// バリデーションエラーの作成
static createValidationError(fieldErrors) {
const errorMessages = Object.entries(fieldErrors)
.map(
([field, errors]) =>
`${field}: ${
Array.isArray(errors)
? errors.join(', ')
: errors
}`
)
.join('; ');
return new ValidationError(
`Validation failed: ${errorMessages}`,
fieldErrors,
{
context: {
validatedFields: Object.keys(fieldErrors),
},
}
);
}
// データベースエラーの作成
static createDatabaseError(
originalError,
operation,
additionalContext = {}
) {
const dbDetails = {
operation,
originalCode: originalError.code,
originalMessage: originalError.message,
...additionalContext,
};
let errorMessage = `Database ${operation} failed`;
let errorCode = 'DATABASE_ERROR';
// データベース固有のエラーコード分析
if (
originalError.code === 'ER_DUP_ENTRY' ||
originalError.code === '23505'
) {
errorMessage = 'Duplicate entry violation';
errorCode = 'ER_DUP_ENTRY';
} else if (
originalError.code === 'ER_NO_REFERENCED_ROW' ||
originalError.code === '23503'
) {
errorMessage = 'Foreign key constraint violation';
errorCode = 'ER_NO_REFERENCED_ROW';
}
return new DatabaseError(errorMessage, dbDetails, {
code: errorCode,
});
}
// HTTP エラーの作成
static createHttpError(
statusCode,
message,
url,
additionalDetails = {}
) {
const httpDetails = {
statusCode,
url,
...additionalDetails,
};
return new HttpError(
message || `HTTP ${statusCode} Error`,
httpDetails
);
}
// 外部サービスエラーの作成
static createExternalServiceError(
serviceName,
originalError,
serviceDetails = {}
) {
return new ExternalServiceError(
serviceName,
`External service '${serviceName}' failed: ${originalError.message}`,
{
originalError: originalError.message,
originalCode: originalError.code,
...serviceDetails,
}
);
}
}
// エラー分析とレポート機能
class ErrorAnalyzer {
constructor() {
this.errorStats = new Map();
}
// エラーの分析と記録
analyzeError(error) {
const analysis = {
type: error.constructor.name,
code: error.code,
severity: error.getSeverity
? error.getSeverity()
: 'unknown',
isOperational: error.isOperational,
timestamp:
error.timestamp || new Date().toISOString(),
context: error.context || {},
};
// 統計情報の更新
this.updateErrorStats(analysis);
return analysis;
}
updateErrorStats(analysis) {
const key = `${analysis.type}:${analysis.code}`;
const stats = this.errorStats.get(key) || {
count: 0,
firstOccurrence: analysis.timestamp,
lastOccurrence: analysis.timestamp,
severity: analysis.severity,
};
stats.count++;
stats.lastOccurrence = analysis.timestamp;
this.errorStats.set(key, stats);
}
// エラー統計レポートの生成
generateReport() {
const report = {
totalErrors: 0,
errorsByType: {},
topErrors: [],
generatedAt: new Date().toISOString(),
};
for (const [key, stats] of this.errorStats.entries()) {
const [type, code] = key.split(':');
report.totalErrors += stats.count;
if (!report.errorsByType[type]) {
report.errorsByType[type] = 0;
}
report.errorsByType[type] += stats.count;
report.topErrors.push({
type,
code,
count: stats.count,
firstOccurrence: stats.firstOccurrence,
lastOccurrence: stats.lastOccurrence,
severity: stats.severity,
});
}
// エラー頻度でソート
report.topErrors.sort((a, b) => b.count - a.count);
report.topErrors = report.topErrors.slice(0, 10); // 上位10件
return report;
}
}
// 使用例
function demonstrateCustomErrors() {
const analyzer = new ErrorAnalyzer();
try {
// バリデーションエラーの例
const validationError =
ErrorFactory.createValidationError({
email: [
'必須項目です',
'有効なメールアドレスを入力してください',
],
age: ['18歳以上である必要があります'],
});
throw validationError;
} catch (error) {
console.error(
'バリデーションエラー:',
error.toLogFormat()
);
analyzer.analyzeError(error);
}
try {
// データベースエラーの例
const dbError = new Error('Duplicate entry');
dbError.code = 'ER_DUP_ENTRY';
const databaseError = ErrorFactory.createDatabaseError(
dbError,
'INSERT',
{ table: 'users', constraint: 'email_unique' }
);
throw databaseError;
} catch (error) {
console.error(
'データベースエラー:',
error.toLogFormat()
);
analyzer.analyzeError(error);
}
try {
// HTTP エラーの例
const httpError = ErrorFactory.createHttpError(
404,
'User not found',
'https://api.example.com/users/123',
{ method: 'GET', userId: 123 }
);
throw httpError;
} catch (error) {
console.error('HTTP エラー:', error.toLogFormat());
analyzer.analyzeError(error);
}
// エラー統計レポートの出力
const report = analyzer.generateReport();
console.log('\n=== エラー分析レポート ===');
console.log(JSON.stringify(report, null, 2));
}
運用レベルエラー処理:ログ・監視・リカバリー戦略
本番環境において堅牢なエラー処理を実現するためには、包括的なログ記録、リアルタイム監視、そして自動回復機能の実装が不可欠です。これらの要素を統合したエラー処理戦略により、システムの可用性と信頼性を大幅に向上させることができます。
構造化ログとエラートラッキング
運用環境での効果的なエラー分析を可能にする構造化ログシステムを実装します。
javascriptconst winston = require('winston');
const path = require('path');
class OperationalErrorHandler {
constructor(options = {}) {
this.options = {
logLevel:
options.logLevel ||
(process.env.NODE_ENV === 'production'
? 'info'
: 'debug'),
logDirectory: options.logDirectory || './logs',
enableFileLogging:
options.enableFileLogging !== false,
enableConsoleLogging:
options.enableConsoleLogging !== false,
maxLogFiles: options.maxLogFiles || 30,
maxLogSize: options.maxLogSize || '100m',
errorThreshold: options.errorThreshold || 10, // 分間エラー数しきい値
...options,
};
this.errorCounts = new Map();
this.circuitBreakers = new Map();
this.setupLogger();
this.setupErrorTracking();
}
setupLogger() {
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS',
}),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf((info) => {
// 構造化ログフォーマット
const logEntry = {
timestamp: info.timestamp,
level: info.level,
message: info.message,
service: process.env.SERVICE_NAME || 'node-app',
version: process.env.SERVICE_VERSION || '1.0.0',
environment:
process.env.NODE_ENV || 'development',
pid: process.pid,
hostname: require('os').hostname(),
...info,
};
return JSON.stringify(logEntry);
})
);
const transports = [];
// コンソール出力
if (this.options.enableConsoleLogging) {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
})
);
}
// ファイル出力
if (this.options.enableFileLogging) {
// 一般ログ
transports.push(
new winston.transports.File({
filename: path.join(
this.options.logDirectory,
'application.log'
),
maxsize: this.options.maxLogSize,
maxFiles: this.options.maxLogFiles,
format: logFormat,
})
);
// エラー専用ログ
transports.push(
new winston.transports.File({
filename: path.join(
this.options.logDirectory,
'error.log'
),
level: 'error',
maxsize: this.options.maxLogSize,
maxFiles: this.options.maxLogFiles,
format: logFormat,
})
);
// 重要度の高いエラー専用ログ
transports.push(
new winston.transports.File({
filename: path.join(
this.options.logDirectory,
'critical.log'
),
level: 'error',
maxsize: this.options.maxLogSize,
maxFiles: this.options.maxLogFiles,
format: logFormat,
// 重要なエラーのみフィルター
filter: (info) => {
return (
info.severity === 'critical' ||
info.level === 'error'
);
},
})
);
}
this.logger = winston.createLogger({
level: this.options.logLevel,
format: logFormat,
transports,
// 未処理のPromise拒否もキャッチ
handleExceptions: true,
handleRejections: true,
});
}
setupErrorTracking() {
// エラー頻度の監視
setInterval(() => {
this.checkErrorThresholds();
}, 60000); // 1分ごと
// 古いエラーカウントのクリーンアップ
setInterval(() => {
this.cleanupOldErrorCounts();
}, 300000); // 5分ごと
}
logError(error, context = {}) {
const errorInfo = this.extractErrorInfo(error);
const logContext = {
...context,
errorInfo,
correlationId:
context.correlationId ||
this.generateCorrelationId(),
userId: context.userId,
sessionId: context.sessionId,
requestId: context.requestId,
userAgent: context.userAgent,
ipAddress: context.ipAddress,
stackTrace: error.stack,
};
// エラーレベルの判定
const level = this.determineLogLevel(error);
this.logger.log(level, error.message, logContext);
// エラー頻度の記録
this.recordErrorOccurrence(errorInfo);
// 重要なエラーの場合は即座にアラート
if (
level === 'error' &&
errorInfo.severity === 'critical'
) {
this.triggerCriticalErrorAlert(errorInfo, logContext);
}
return logContext.correlationId;
}
extractErrorInfo(error) {
const errorInfo = {
name: error.name || 'Error',
code: error.code || 'UNKNOWN_ERROR',
statusCode: error.statusCode || 500,
severity: 'medium',
isOperational: true,
category: 'application',
};
// ApplicationError の場合
if (error.isApplicationError) {
errorInfo.severity = error.getSeverity();
errorInfo.isOperational = error.isOperational;
errorInfo.details = error.details;
errorInfo.context = error.context;
}
// エラーカテゴリの判定
if (
error.name.includes('Database') ||
error.code?.startsWith('ER_')
) {
errorInfo.category = 'database';
} else if (
error.name.includes('Network') ||
error.name.includes('Http')
) {
errorInfo.category = 'network';
} else if (error.name.includes('Validation')) {
errorInfo.category = 'validation';
} else if (error.name.includes('Auth')) {
errorInfo.category = 'authentication';
} else if (
error.code === 'ENOENT' ||
error.code === 'EACCES'
) {
errorInfo.category = 'filesystem';
}
return errorInfo;
}
determineLogLevel(error) {
if (
!error.isOperational ||
error.getSeverity?.() === 'critical'
) {
return 'error';
}
if (error.statusCode >= 500) {
return 'error';
}
if (error.statusCode >= 400) {
return 'warn';
}
return 'info';
}
recordErrorOccurrence(errorInfo) {
const key = `${errorInfo.category}:${errorInfo.code}`;
const minute = Math.floor(Date.now() / 60000);
const timeKey = `${key}:${minute}`;
const count = this.errorCounts.get(timeKey) || 0;
this.errorCounts.set(timeKey, count + 1);
}
checkErrorThresholds() {
const currentMinute = Math.floor(Date.now() / 60000);
const alerts = [];
for (const [key, count] of this.errorCounts.entries()) {
const [category, code, minute] = key.split(':');
if (
parseInt(minute) === currentMinute &&
count >= this.options.errorThreshold
) {
alerts.push({
category,
code,
count,
threshold: this.options.errorThreshold,
minute: currentMinute,
});
// サーキットブレーカーの作動
this.activateCircuitBreaker(category, code);
}
}
if (alerts.length > 0) {
this.triggerThresholdAlert(alerts);
}
}
cleanupOldErrorCounts() {
const currentMinute = Math.floor(Date.now() / 60000);
const cutoffMinute = currentMinute - 10; // 10分前より古いデータを削除
for (const key of this.errorCounts.keys()) {
const minute = parseInt(key.split(':')[2]);
if (minute < cutoffMinute) {
this.errorCounts.delete(key);
}
}
}
activateCircuitBreaker(category, code) {
const key = `${category}:${code}`;
if (!this.circuitBreakers.has(key)) {
this.circuitBreakers.set(key, {
isOpen: true,
openedAt: Date.now(),
failureCount: 0,
});
this.logger.warn('Circuit breaker activated', {
category,
code,
reason: 'Error threshold exceeded',
});
// 自動回復タイマー
setTimeout(() => {
this.resetCircuitBreaker(key);
}, 30000); // 30秒後に自動復旧を試行
}
}
resetCircuitBreaker(key) {
if (this.circuitBreakers.has(key)) {
this.circuitBreakers.delete(key);
const [category, code] = key.split(':');
this.logger.info('Circuit breaker reset', {
category,
code,
reason: 'Automatic recovery timeout',
});
}
}
isCircuitBreakerOpen(category, code) {
const key = `${category}:${code}`;
return this.circuitBreakers.has(key);
}
triggerCriticalErrorAlert(errorInfo, context) {
const alertPayload = {
type: 'critical_error',
severity: 'critical',
service: process.env.SERVICE_NAME || 'node-app',
environment: process.env.NODE_ENV || 'development',
timestamp: new Date().toISOString(),
errorInfo,
context: {
correlationId: context.correlationId,
userId: context.userId,
requestId: context.requestId,
ipAddress: context.ipAddress,
},
};
// 外部アラートシステムへの送信
this.sendExternalAlert(alertPayload);
this.logger.error(
'Critical error alert triggered',
alertPayload
);
}
triggerThresholdAlert(alerts) {
const alertPayload = {
type: 'threshold_exceeded',
severity: 'high',
service: process.env.SERVICE_NAME || 'node-app',
environment: process.env.NODE_ENV || 'development',
timestamp: new Date().toISOString(),
alerts,
};
this.sendExternalAlert(alertPayload);
this.logger.warn(
'Error threshold exceeded',
alertPayload
);
}
async sendExternalAlert(alertPayload) {
try {
// Slack、Discord、PagerDuty などへの送信
if (process.env.ALERT_WEBHOOK_URL) {
const response = await fetch(
process.env.ALERT_WEBHOOK_URL,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(alertPayload),
}
);
if (!response.ok) {
throw new Error(
`Alert webhook failed: ${response.status}`
);
}
}
// メール送信
if (process.env.ALERT_EMAIL_ENABLED === 'true') {
await this.sendEmailAlert(alertPayload);
}
} catch (error) {
this.logger.error('Failed to send external alert', {
error: error.message,
alertPayload,
});
}
}
async sendEmailAlert(alertPayload) {
// メール送信の実装(例:nodemailer)
console.log('Email alert would be sent:', alertPayload);
}
generateCorrelationId() {
return `${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
}
// ヘルスチェック機能
getHealthStatus() {
const recentMinute = Math.floor(Date.now() / 60000);
let totalRecentErrors = 0;
for (const [key, count] of this.errorCounts.entries()) {
const minute = parseInt(key.split(':')[2]);
if (minute === recentMinute) {
totalRecentErrors += count;
}
}
const activeCircuitBreakers = this.circuitBreakers.size;
return {
status:
totalRecentErrors < this.options.errorThreshold &&
activeCircuitBreakers === 0
? 'healthy'
: 'degraded',
timestamp: new Date().toISOString(),
metrics: {
recentErrors: totalRecentErrors,
errorThreshold: this.options.errorThreshold,
activeCircuitBreakers,
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
},
};
}
}
// 使用例
async function demonstrateOperationalErrorHandling() {
const errorHandler = new OperationalErrorHandler({
logLevel: 'debug',
logDirectory: './logs',
errorThreshold: 5,
});
try {
// 様々なエラーをシミュレート
const errors = [
new ValidationError('Invalid email format', {
email: ['Invalid format'],
}),
new DatabaseError('Connection timeout', {
operation: 'SELECT',
}),
new HttpError('Service unavailable', {
statusCode: 503,
url: 'https://api.example.com',
}),
new AuthenticationError('Invalid credentials', {
username: 'testuser',
}),
];
for (const error of errors) {
const correlationId = errorHandler.logError(error, {
userId: '12345',
requestId: 'req-' + Date.now(),
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0...',
});
console.log(`エラーログ記録完了: ${correlationId}`);
}
// ヘルスチェック
const healthStatus = errorHandler.getHealthStatus();
console.log('ヘルスチェック結果:', healthStatus);
} catch (error) {
console.error(
'運用エラーハンドリングでエラー:',
error.message
);
}
}
まとめ(堅牢な Node.js アプリケーションのエラー戦略)
本記事では、Node.js における包括的なエラー処理パターンを体系的に解説いたしました。適切なエラーハンドリングは、単なる例外処理を超えて、アプリケーションの信頼性、保守性、そして運用効率を決定する重要な要素であることをご理解いただけたでしょう。
エラー処理の重要なポイント
段階的なアプローチが効果的です。基本的な try/catch から始まり、非同期処理、イベント駆動、グローバルハンドラーまで、各レイヤーでの適切な処理により、多重防御システムを構築できます。
カスタムエラークラスの設計により、エラーの分類と詳細な情報管理が可能になります。階層的なエラー構造は、デバッグ効率の向上と運用時のトラブルシューティングを大幅に改善いたします。
運用レベルでの監視では、構造化ログ、リアルタイム監視、自動回復機能の統合により、本番環境での安定性を確保できます。エラーの早期発見と適切な対応により、サービスの可用性を維持することが可能です。
実践的な運用指針
実際のプロジェクトでは、開発環境と本番環境での異なるエラー処理戦略が必要です。開発時には詳細なデバッグ情報を提供し、本番環境では適切な情報隠蔽とログ記録を行うことで、セキュリティと運用効率の両立が実現できます。
継続的な改善も重要な要素です。エラーパターンの分析、監視データの活用、チーム内でのベストプラクティス共有により、より堅牢なシステムへと発展させていくことができます。
Node.js の非同期処理特性を理解し、本記事で紹介したパターンを適切に組み合わせることで、信頼性の高いアプリケーションを構築していただけることを願っております。エラー処理は一度の実装で完結するものではなく、継続的な改善とメンテナンスが重要であることを念頭に置いて、日々の開発に取り組んでいただければと思います。
関連リンク
公式ドキュメント
- Node.js Error Handling - Node.js 公式エラーハンドリングガイド
- Express Error Handling - Express 公式エラーハンドリング
- Fastify Error Handling - Fastify 公式エラーハンドリング
ログ・監視ツール
- Winston Logger - Node.js 用構造化ログライブラリ
- Pino Logger - 高性能 JSON ロガー
- New Relic Node.js - APM 監視ツール
エラー追跡サービス
ベストプラクティス
- Node.js Best Practices - Node.js ベストプラクティス(エラーハンドリング編)
- 12 Factor App - クラウドネイティブアプリケーションのログ戦略
- blog
「QA は最後の砦」という幻想を捨てる。開発プロセスに QA を組み込み、手戻りをなくす方法
- blog
ドキュメントは「悪」じゃない。アジャイル開発で「ちょうどいい」ドキュメントを見つけるための思考法
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ