適切なエラーハンドリングは、本番環境で安定して動作するアプリケーションを作る上で不可欠です。
この記事では、Node.jsにおける包括的なエラー処理の方法を学びます。
💡この記事でわかる事
以下の内容を習得することができます:
- Node.jsのエラーの種類
- 同期処理のエラーハンドリング
- コールバックのエラーハンドリング
- Promiseのエラーハンドリング
- async/awaitのエラーハンドリング
- EventEmitterのエラーハンドリング
- 未処理エラーの対策
- エラーハンドリングのベストプラクティス
Contents
1. Node.jsのエラーの種類
1.1 エラーの分類
Node.jsで発生するエラーは大きく4つに分類されます。
1. 標準JavaScriptエラー
// SyntaxError: 構文エラー
eval('const x = ;');
// ReferenceError: 未定義の変数
console.log(undefinedVariable);
// TypeError: 型エラー
null.toString();
// RangeError: 範囲エラー
new Array(-1);2. システムエラー
- ファイルが存在しない(ENOENT)
- 権限がない(EACCES)
- アドレスが使用中(EADDRINUSE)
const fs = require('fs');
fs.readFile('/non-existent-file.txt', (err, data) => {
if (err) {
console.log(err.code);// => 'ENOENT'
console.log(err.message);// => "ENOENT: no such file or directory..."
}
});3. ユーザー定義エラー
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
throw new ValidationError('入力値が不正です');4. アサーションエラー
const assert = require('assert');
// テストやデバッグで使用
assert.strictEqual(1, 2);// => AssertionError1.2 Errorオブジェクトの構造
const error = new Error('何か問題が発生しました');
console.log(error.name);// => 'Error'
console.log(error.message);// => '何か問題が発生しました'
console.log(error.stack);// => スタックトレース
// カスタムプロパティを追加可能
error.statusCode = 500;
error.isOperational = true;2. 同期処理のエラーハンドリング
2.1 try-catch の基本
同期的なコードのエラーはtry-catchでキャッチできます。
const fs = require('fs');
try {
const data = fs.readFileSync('file.txt', 'utf8');
const parsed = JSON.parse(data);
console.log(parsed);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('ファイルが見つかりません');
} else if (err instanceof SyntaxError) {
console.error('JSONの解析に失敗しました');
} else {
console.error('予期しないエラー:', err);
}
}2.2 複数のエラーを処理
function processUserData(userData) {
try {
// バリデーション
if (!userData.email) {
throw new Error('メールアドレスが必要です');
}
if (!userData.email.includes('@')) {
throw new Error('無効なメールアドレス形式です');
}
// 処理を続行
return { success: true, data: userData };
} catch (err) {
return { success: false, error: err.message };
}
}
// 使用例
console.log(processUserData({ email: 'test@example.com' }));
// => { success: true, data: { email: 'test@example.com' } }
console.log(processUserData({ email: 'invalid' }));
// => { success: false, error: '無効なメールアドレス形式です' }2.3 finallyブロック
finallyブロックは、エラーの有無に関わらず必ず実行されます。
const fs = require('fs');
let fileHandle;
try {
fileHandle = fs.openSync('file.txt', 'r');
const data = fs.readFileSync(fileHandle, 'utf8');
console.log(data);
} catch (err) {
console.error('エラー:', err.message);
} finally {
// リソースのクリーンアップ
if (fileHandle !== undefined) {
fs.closeSync(fileHandle);
console.log('ファイルを閉じました');
}
}3. コールバックのエラーハンドリング
3.1 Node.jsスタイルのコールバック
Node.jsのコールバックは「エラーファースト」パターンを使います。
const fs = require('fs');
// 第1引数がエラー、第2引数が結果
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
// エラー処理
console.error('ファイル読み込みエラー:', err.message);
return;
}
// 成功時の処理
console.log('ファイル内容:', data);
});3.2 コールバック地獄の回避
ネストが深くなる「コールバック地獄」を避ける方法です。
const fs = require('fs');
// 悪い例: コールバック地獄
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) return console.error(err);
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) return console.error(err);
fs.readFile('file3.txt', 'utf8', (err, data3) => {
if (err) return console.error(err);
console.log(data1, data2, data3);
});
});
});
// 良い例: 関数を分割
function readFile1(callback) {
fs.readFile('file1.txt', 'utf8', (err, data) => {
if (err) return callback(err);
callback(null, data);
});
}
function readFile2(data1, callback) {
fs.readFile('file2.txt', 'utf8', (err, data) => {
if (err) return callback(err);
callback(null, data1, data);
});
}
function readFile3(data1, data2, callback) {
fs.readFile('file3.txt', 'utf8', (err, data) => {
if (err) return callback(err);
callback(null, data1, data2, data);
});
}
// 実行
readFile1((err, data1) => {
if (err) return console.error(err);
readFile2(data1, (err, data1, data2) => {
if (err) return console.error(err);
readFile3(data1, data2, (err, data1, data2, data3) => {
if (err) return console.error(err);
console.log(data1, data2, data3);
});
});
});4. Promiseのエラーハンドリング
4.1 .catch()でエラーを処理
Promiseのエラーは.catch()でキャッチします。
const fs = require('fs').promises;
fs.readFile('file.txt', 'utf8')
.then(data => {
console.log('ファイル内容:', data);
return JSON.parse(data);
})
.then(parsed => {
console.log('パース結果:', parsed);
})
.catch(err => {
// すべてのエラーをここでキャッチ
console.error('エラーが発生しました:', err.message);
});4.2 複数のPromiseのエラーハンドリング
const fs = require('fs').promises;
// Promise.all() - 1つでも失敗したら全体が失敗
Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
])
.then(([data1, data2, data3]) => {
console.log('すべて成功:', data1, data2, data3);
})
.catch(err => {
console.error('いずれかが失敗:', err.message);
});
// Promise.allSettled() - すべての結果を取得(成功/失敗問わず)
Promise.allSettled([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('non-existent.txt', 'utf8')
])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`ファイル${index + 1}: 成功`);
} else {
console.log(`ファイル${index + 1}: 失敗 - ${result.reason.message}`);
}
});
});4.3 カスタムPromiseのエラー処理
function readFileWithRetry(filePath, retries = 3) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
if (n === 0) {
reject(new Error(`${retries}回試行しましたが失敗しました`));
} else {
console.log(`リトライ中... 残り${n}回`);
setTimeout(() => attempt(n - 1), 1000);
}
} else {
resolve(data);
}
});
};
attempt(retries);
});
}
// 使用例
readFileWithRetry('file.txt')
.then(data => console.log('成功:', data))
.catch(err => console.error('失敗:', err.message));5. async/awaitのエラーハンドリング
5.1 基本的なtry-catch
async/awaitではtry-catchを使ってエラーをキャッチします。
const fs = require('fs').promises;
async function readAndParseFile(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8');
const parsed = JSON.parse(data);
return parsed;
} catch (err) {
console.error('エラー:', err.message);
throw err;// エラーを再スロー
}
}
// 使用例
(async () => {
try {
const result = await readAndParseFile('data.json');
console.log('結果:', result);
} catch (err) {
console.error('処理に失敗しました');
}
})();5.2 複数の非同期処理
const fs = require('fs').promises;
async function processMultipleFiles() {
try {
// 並行実行
const [data1, data2, data3] = await Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
]);
console.log('すべてのファイルを読み込みました');
return { data1, data2, data3 };
} catch (err) {
console.error('ファイル読み込み失敗:', err.message);
throw err;
}
}
// 順次実行
async function processFilesSequentially() {
const results = [];
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
for (const file of files) {
try {
const data = await fs.readFile(file, 'utf8');
results.push(data);
} catch (err) {
console.error(`${file}の読み込み失敗:`, err.message);
// 失敗しても続行
results.push(null);
}
}
return results;
}5.3 エラーハンドリングのヘルパー関数
エラーハンドリングを簡潔にするヘルパー関数です。
// エラーを配列形式で返すヘルパー
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
// 使用例
const fs = require('fs').promises;
async function readFile() {
const [err, data] = await to(fs.readFile('file.txt', 'utf8'));
if (err) {
console.error('エラー:', err.message);
return null;
}
console.log('成功:', data);
return data;
}
// 複数の処理
async function processData() {
const [readErr, fileData] = await to(fs.readFile('data.json', 'utf8'));
if (readErr) return console.error('読み込み失敗');
const [parseErr, parsed] = await to(JSON.parse(fileData));
if (parseErr) return console.error('解析失敗');
console.log('成功:', parsed);
}6. EventEmitterのエラーハンドリング
6.1 errorイベントの処理
EventEmitterのerrorイベントは特別で、リスナーがないとプロセスがクラッシュします。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
// 悪い例: errorリスナーがない
// emitter.emit('error', new Error('何か問題が発生'));
// => プロセスがクラッシュ!
// 良い例: errorリスナーを登録
emitter.on('error', (err) => {
console.error('エラーをキャッチしました:', err.message);
});
emitter.emit('error', new Error('何か問題が発生'));
// => エラーをキャッチしました: 何か問題が発生6.2 複数のエラーハンドラ
const EventEmitter = require('events');
class DatabaseConnection extends EventEmitter {
connect() {
// 接続処理をシミュレート
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
this.emit('connected');
} else {
this.emit('error', new Error('接続に失敗しました'));
}
}, 1000);
}
}
const db = new DatabaseConnection();
// メインのエラーハンドラ
db.on('error', (err) => {
console.error('❌ データベースエラー:', err.message);
});
// ログ用のエラーハンドラ
db.on('error', (err) => {
// ログファイルに記録
console.log(`[${new Date().toISOString()}] ERROR: ${err.message}`);
});
// 接続成功ハンドラ
db.on('connected', () => {
console.log('✅ データベースに接続しました');
});
db.connect();6.3 Streamのエラーハンドリング
Streamもイベントベースなので、適切なエラー処理が必要です。
const fs = require('fs');
const { pipeline } = require('stream');
// 悪い例: エラーハンドリングなし
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
// readStream.pipe(writeStream); // エラーが起きたらクラッシュ
// 良い例: 各ストリームにエラーハンドラ
readStream.on('error', (err) => {
console.error('読み込みエラー:', err.message);
});
writeStream.on('error', (err) => {
console.error('書き込みエラー:', err.message);
});
readStream.pipe(writeStream);
// さらに良い例: pipeline()を使う
pipeline(
fs.createReadStream('input.txt'),
fs.createWriteStream('output.txt'),
(err) => {
if (err) {
console.error('パイプラインエラー:', err.message);
} else {
console.log('コピー完了');
}
}
);7. 未処理エラーの対策
7.1 未処理のPromise拒否
Promiseが拒否されたのに.catch()で処理されないケースです。
// 未処理の拒否を検出
process.on('unhandledRejection', (reason, promise) => {
console.error('未処理のPromise拒否:', reason);
console.error('Promise:', promise);
// 本番環境ではログを記録して適切に処理
// 場合によってはプロセスを終了
});
// 未処理の拒否を発生させる例
Promise.reject(new Error('処理されていないエラー'));
// 正しい例
Promise.reject(new Error('適切に処理されたエラー'))
.catch(err => {
console.error('エラーを処理:', err.message);
});7.2 未捕捉の例外
try-catchで捕捉されなかった例外です。
// 未捕捉の例外を検出
process.on('uncaughtException', (err) => {
console.error('未捕捉の例外:', err);
console.error('スタックトレース:', err.stack);
// クリーンアップ処理
// データベース接続を閉じる、ファイルを保存するなど
// プロセスを終了(推奨)
process.exit(1);
});
// 未捕捉の例外を発生させる例
setTimeout(() => {
throw new Error('未捕捉のエラー');
}, 1000);7.3 グレースフルシャットダウン
エラー発生時に適切にアプリケーションを終了させる方法です。
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello World');
});
server.listen(3000);
// シグナルハンドラの設定
function gracefulShutdown(signal) {
console.log(`\n${signal}を受信しました。シャットダウンを開始します...`);
server.close(() => {
console.log('HTTPサーバーを停止しました');
// その他のクリーンアップ
// - データベース接続を閉じる
// - 進行中のタスクを完了させる
// - ログをフラッシュする
console.log('クリーンアップ完了。プロセスを終了します。');
process.exit(0);
});
// タイムアウト設定(10秒以内に終了しない場合は強制終了)
setTimeout(() => {
console.error('シャットダウンがタイムアウトしました。強制終了します。');
process.exit(1);
}, 10000);
}
// シグナルをリッスン
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 未処理エラーのハンドリング
process.on('unhandledRejection', (reason) => {
console.error('未処理のPromise拒否:', reason);
gracefulShutdown('unhandledRejection');
});
process.on('uncaughtException', (err) => {
console.error('未捕捉の例外:', err);
gracefulShutdown('uncaughtException');
});
console.log('サーバー起動: http://localhost:3000');8. エラーハンドリングのベストプラクティス
8.1 カスタムエラークラスの作成
目的別にエラークラスを作成すると、エラー処理が明確になります。
// 基底エラークラス
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
// 具体的なエラークラス
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
}
}
class DatabaseError extends AppError {
constructor(message) {
super(message, 500, false); <em>// 非操作的エラー</em>
}
}
// 使用例
function findUser(userId) {
if (!userId) {
throw new ValidationError('ユーザーIDが必要です');
}
// データベース検索をシミュレート
const user = null;
if (!user) {
throw new NotFoundError(`ユーザーID ${userId} が見つかりません`);
}
return user;
}
// エラーハンドリング
try {
const user = findUser(null);
} catch (err) {
if (err instanceof ValidationError) {
console.log('入力エラー:', err.message);
} else if (err instanceof NotFoundError) {
console.log('リソースが見つかりません:', err.message);
} else if (err instanceof DatabaseError) {
console.log('システムエラー:', err.message);
// 管理者に通知
} else {
console.log('予期しないエラー:', err);
}
}8.2 集中エラーハンドリングミドルウェア
Expressなどのフレームワークで使える集中管理の例です。
const express = require('express');
const app = express();
// ルート定義
app.get('/user/:id', async (req, res, next) => {
try {
const userId = req.params.id;
if (!userId) {
throw new ValidationError('ユーザーIDが必要です');
}
// ユーザー検索(例)
const user = await findUser(userId);
res.json(user);
} catch (err) {
next(err);// エラーを次のミドルウェアに渡す
}
});
// エラーハンドリングミドルウェア
app.use((err, req, res, next) => {
// ログ記録
console.error({
timestamp: new Date().toISOString(),
error: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
// 操作的エラーかどうかで処理を分ける
if (err.isOperational) {
// クライアントに安全に返せるエラー
res.status(err.statusCode || 500).json({
success: false,
message: err.message
});
} else {
// プログラムエラー(バグ)
res.status(500).json({
success: false,
message: '内部サーバーエラーが発生しました'
});
// 本番環境では、非操作的エラーの場合プロセスを再起動
if (process.env.NODE_ENV === 'production') {
process.exit(1);
}
}
});
app.listen(3000);8.3 エラーログの記録
適切なログ記録は、問題の特定と修正に不可欠です。
const fs = require('fs');
const path = require('path');
class Logger {
constructor(logDir = './logs') {
this.logDir = logDir;
// ログディレクトリを作成
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
log(level, message, meta = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
...meta
};
// コンソールに出力
console.log(JSON.stringify(logEntry));
// ファイルに記録
const logFile = path.join(
this.logDir,
`${level}-${new Date().toISOString().split('T')[0]}.log`
);
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
}
error(message, error) {
this.log('error', message, {
errorMessage: error.message,
stack: error.stack,
name: error.name
});
}
warn(message, meta) {
this.log('warn', message, meta);
}
info(message, meta) {
this.log('info', message, meta);
}
}
// 使用例
const logger = new Logger();
try {
throw new Error('テストエラー');
} catch (err) {
logger.error('処理中にエラーが発生しました', err);
}8.4 リトライロジック
一時的なエラーに対してリトライする実装です。
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxAttempts) {
throw new Error(`${maxAttempts}回試行しましたが失敗しました: ${err.message}`);
}
console.log(`試行 ${attempt} 失敗。${delay}ms後にリトライします...`);
await new Promise(resolve => setTimeout(resolve, delay));
// 指数バックオフ(オプション)
delay *= 2;
}
}
}
// 使用例
async function fetchData() {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
retry(fetchData, 3, 1000)
.then(data => console.log('成功:', data))
.catch(err => console.error('最終的に失敗:', err.message));実践例:堅牢なAPIサーバー
これまでの知識を活用した、エラーハンドリングが充実したAPIサーバーの例です。
const http = require('http');
const url = require('url');
// カスタムエラークラス
class APIError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
// ロガー
class Logger {
error(message, error) {
console.error(`[ERROR] ${message}`, {
message: error.message,
stack: error.stack
});
}
info(message) {
console.log(`[INFO] ${message}`);
}
}
const logger = new Logger();
// データベース操作のシミュレーション
async function getUser(userId) {
// バリデーション
if (!userId || typeof userId !== 'string') {
throw new APIError('無効なユーザーIDです', 400);
}
// データベース検索をシミュレート
await new Promise(resolve => setTimeout(resolve, 100));
// ランダムでエラーを発生させる(テスト用)
const random = Math.random();
if (random < 0.2) {
throw new Error('データベース接続エラー');
}
// ユーザーが見つからない場合
if (userId === '999') {
throw new APIError('ユーザーが見つかりません', 404);
}
return {
id: userId,
name: `ユーザー${userId}`,
email: `user${userId}@example.com`
};
}
// リクエストハンドラ
async function handleRequest(req, res) {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
try {
// ルーティング
if (pathname === '/api/user' && req.method === 'GET') {
const userId = parsedUrl.query.id;
if (!userId) {
throw new APIError('ユーザーIDパラメータが必要です', 400);
}
const user = await getUser(userId);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: user
}));
} else if (pathname === '/api/health' && req.method === 'GET') {
// ヘルスチェックエンドポイント
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
status: 'healthy',
timestamp: new Date().toISOString()
}));
} else {
throw new APIError('エンドポイントが見つかりません', 404);
}
} catch (err) {
handleError(err, req, res);
}
}
// エラーハンドリング関数
function handleError(err, req, res) {
// エラーをログに記録
logger.error(`リクエスト処理エラー [${req.method} ${req.url}]`, err);
// APIエラー(予期されたエラー)
if (err instanceof APIError) {
res.writeHead(err.statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: {
message: err.message,
statusCode: err.statusCode
}
}));
} else {
// 予期しないエラー
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: {
message: '内部サーバーエラーが発生しました',
statusCode: 500
}
}));
}
}
// サーバーを作成
const server = http.createServer(handleRequest);
// グレースフルシャットダウン
function gracefulShutdown(signal) {
logger.info(`${signal}を受信。シャットダウンを開始します...`);
server.close(() => {
logger.info('サーバーを停止しました');
process.exit(0);
});
// タイムアウト
setTimeout(() => {
logger.error('シャットダウンがタイムアウトしました');
process.exit(1);
}, 10000);
}
// プロセスイベントハンドラ
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('unhandledRejection', (reason, promise) => {
logger.error('未処理のPromise拒否', new Error(reason));
gracefulShutdown('unhandledRejection');
});
process.on('uncaughtException', (err) => {
logger.error('未捕捉の例外', err);
gracefulShutdown('uncaughtException');
});
// サーバー起動
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
logger.info(`APIサーバー起動: http://localhost:${PORT}`);
logger.info('エンドポイント:');
logger.info(' GET /api/user?id={userId}');
logger.info(' GET /api/health');
});
// サーバーエラーハンドリング
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
logger.error('ポートが既に使用されています', err);
} else {
logger.error('サーバーエラー', err);
}
process.exit(1);
});テスト方法:
# 正常なリクエスト
curl "http://localhost:3000/api/user?id=123"
# ユーザーが見つからない(404)
curl "http://localhost:3000/api/user?id=999"
# パラメータ不足(400)
curl "http://localhost:3000/api/user"
# エンドポイントが存在しない(404)
curl "http://localhost:3000/api/invalid"
# ヘルスチェック
curl "http://localhost:3000/api/health"まとめ
堅牢なエラーハンドリングは、信頼性の高いNode.jsアプリケーションを構築する上で不可欠です。
重要なポイント:
同期処理
try-catchでエラーをキャッチfinallyでクリーンアップ処理
非同期処理
- コールバック: エラーファーストパターン
- Promise:
.catch()でエラーをキャッチ - async/await:
try-catchを使用
EventEmitter
errorイベントは必ずリスナーを登録- リスナーがないとプロセスがクラッシュ
未処理エラー
unhandledRejection: 未処理のPromise拒否uncaughtException: 未捕捉の例外- グレースフルシャットダウンを実装
ベストプラクティス
- カスタムエラークラスで分類
- 集中エラーハンドリングミドルウェア
- 適切なログ記録
- 一時的なエラーにはリトライロジック
- 操作的エラーと非操作的エラーを区別
- 本番環境では詳細なエラー情報を隠す
次回予告:
第5回では、パフォーマンス最適化テクニックについて解説します。クラスタリング、Worker Threads、メモリ管理、ベンチマークなど、Node.jsアプリケーションを高速化する技術を学びましょう!


























