デザインについての学習メモブログ

Node.js入門(5):堅牢なエラーハンドリング

記事内に広告が含まれています。

Node.js入門(5):堅牢なエラーハンドリング

適切なエラーハンドリングは、本番環境で安定して動作するアプリケーションを作る上で不可欠です。

この記事では、Node.jsにおける包括的なエラー処理の方法を学びます。

💡この記事でわかる事

以下の内容を習得することができます:

  1. Node.jsのエラーの種類
  2. 同期処理のエラーハンドリング
  3. コールバックのエラーハンドリング
  4. Promiseのエラーハンドリング
  5. async/awaitのエラーハンドリング
  6. EventEmitterのエラーハンドリング
  7. 未処理エラーの対策
  8. エラーハンドリングのベストプラクティス

1. Node.jsのエラーの種類

1.1 エラーの分類

Node.jsで発生するエラーは大きく4つに分類されます。

1. 標準JavaScriptエラー

JavaScript
// SyntaxError: 構文エラー
 eval('const x = ;'); 

// ReferenceError: 未定義の変数
console.log(undefinedVariable);

// TypeError: 型エラー
null.toString();

// RangeError: 範囲エラー
new Array(-1);

2. システムエラー

  • ファイルが存在しない(ENOENT)
  • 権限がない(EACCES)
  • アドレスが使用中(EADDRINUSE)
JavaScript
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. ユーザー定義エラー

JavaScript
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

throw new ValidationError('入力値が不正です');

4. アサーションエラー

JavaScript
const assert = require('assert');

// テストやデバッグで使用
assert.strictEqual(1, 2);// => AssertionError

1.2 Errorオブジェクトの構造

JavaScript
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でキャッチできます。

JavaScript
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 複数のエラーを処理

JavaScript
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ブロックは、エラーの有無に関わらず必ず実行されます。

JavaScript
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のコールバックは「エラーファースト」パターンを使います。

JavaScript
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 コールバック地獄の回避

ネストが深くなる「コールバック地獄」を避ける方法です。

JavaScript
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()でキャッチします。

JavaScript
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のエラーハンドリング

JavaScript
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のエラー処理

JavaScript
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を使ってエラーをキャッチします。

JavaScript
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 複数の非同期処理

JavaScript
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 エラーハンドリングのヘルパー関数

エラーハンドリングを簡潔にするヘルパー関数です。

JavaScript
// エラーを配列形式で返すヘルパー
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イベントは特別で、リスナーがないとプロセスがクラッシュします。

JavaScript
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 複数のエラーハンドラ

JavaScript
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もイベントベースなので、適切なエラー処理が必要です。

JavaScript
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()で処理されないケースです。

JavaScript
// 未処理の拒否を検出
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で捕捉されなかった例外です。

JavaScript
// 未捕捉の例外を検出
process.on('uncaughtException', (err) => {
  console.error('未捕捉の例外:', err);
  console.error('スタックトレース:', err.stack);
  
  // クリーンアップ処理
  // データベース接続を閉じる、ファイルを保存するなど
  
  // プロセスを終了(推奨)
  process.exit(1);
});

// 未捕捉の例外を発生させる例
setTimeout(() => {
  throw new Error('未捕捉のエラー');
}, 1000);

7.3 グレースフルシャットダウン

エラー発生時に適切にアプリケーションを終了させる方法です。

JavaScript
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 カスタムエラークラスの作成

目的別にエラークラスを作成すると、エラー処理が明確になります。

JavaScript
// 基底エラークラス
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などのフレームワークで使える集中管理の例です。

JavaScript
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 エラーログの記録

適切なログ記録は、問題の特定と修正に不可欠です。

JavaScript
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 リトライロジック

一時的なエラーに対してリトライする実装です。

JavaScript
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サーバーの例です。

JavaScript
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);
});

テスト方法:

Bash
# 正常なリクエスト
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: 未捕捉の例外
  • グレースフルシャットダウンを実装

ベストプラクティス

  • カスタムエラークラスで分類
  • 集中エラーハンドリングミドルウェア
  • 適切なログ記録
  • 一時的なエラーにはリトライロジック
  • 操作的エラーと非操作的エラーを区別
  • 本番環境では詳細なエラー情報を隠す

次回予告:
第6回では、パフォーマンス最適化テクニックについて解説します。クラスタリング、Worker Threads、メモリ管理、ベンチマークなど、Node.jsアプリケーションを高速化する技術を学びましょう!