NodeJS 速習チュートリアル

Node.js エラーハンドリング

1. なぜエラーをハンドリングするのか?

エラーはどんなプログラムにおいても避けられないものですが、その処理(ハンドリング)の仕方がアプリケーションの品質を左右します。Node.js において適切なエラーハンドリングが極めて重要な理由は以下の通りです:

アプリケーションの予期せぬ*クラッシュ(Crash)を防ぐ

  • ユーザーに対して意味のあるフィードバックを提供する
  • 適切なエラーコンテキスト(Error Context)により、デバッグ(Debugging)を容易にする
  • 本番環境におけるアプリケーションの安定性を維持する
  • リソース(ファイルディスクリプタやメモリなど)が適切にクリーンアップされることを保証する

2. Node.js における一般的なエラーの種類

異なるエラーの種類を理解することで、それぞれに適切な対処が可能になります:

2.1 標準的な JavaScript エラー

// SyntaxError (構文エラー)
JSON.parse('{invalid json}');

// TypeError (型エラー)
null.someProperty;

// ReferenceError (参照エラー)
unknownVariable;

2.2 システムエラー

// ENOENT: ファイルまたはディレクトリが存在しない
const fs = require('fs');
fs.readFile('nonexistent.txt', (err) => {
  console.error(err.code); // 'ENOENT'
});

// ECONNREFUSED: 接続が拒否された
const http = require('http');
const req = http.get('http://nonexistent-site.com', (res) => {});
req.on('error', (err) => {
  console.error(err.code); // 'ECONNREFUSED' または 'ENOTFOUND'
});

3. 基本的なエラーハンドリング

Node.js は、エラーハンドリングのためにいくつかのパターンに従います:

3.1 エラー優先コールバック (Error-First Callbacks)

Node.js のコアモジュール(Module)で最も一般的なパターンであり、コールバックの最初の引数がエラーオブジェクト(発生した場合)になります。

例:エラー優先コールバック

const fs = require('fs');

function readConfigFile(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    if (err) {
      // 特定のエラータイプをハンドリング
      if (err.code === 'ENOENT') {
        return callback(new Error(`設定ファイル ${filename} が見つかりません`));
      } else if (err.code === 'EACCES') {
        return callback(new Error(`${filename} を読み取る権限がありません`));
      }
      // その他のすべてのエラー
      return callback(err);
    }

    // エラーがない場合にデータを処理
    try {
      const config = JSON.parse(data);
      callback(null, config);
    } catch (parseError) {
      callback(new Error(`${filename} 内の JSON が無効です`));
    }
  });
}

// 使用例
readConfigFile('config.json', (err, config) => {
  if (err) {
    console.error('設定の読み込みに失敗しました:', err.message);
    // エラー処理(例:デフォルト設定を使用するなど)
    return;
  }
  console.log('設定が正常にロードされました:', config);
});

4. モダンなエラーハンドリング

4.1 Async/Await と try...catch の併用

Async/Await を使用すると、同期コードと非同期コードの両方に対して try/catch ブロックを使用でき、可読性が高まります。

例:Async/Await による try/catch

const fs = require('fs').promises;

async function loadUserData(userId) {
  try {
    const data = await fs.readFile(`users/${userId}.json`, 'utf8');
    const user = JSON.parse(data);

    if (!user.email) {
      throw new Error('無効なユーザーデータ: メールアドレスがありません');
    }

    return user;
  } catch (error) {
    // 異なるエラータイプをハンドリング
    if (error.code === 'ENOENT') {
      throw new Error(`ユーザー ${userId} が見つかりません`);
    } else if (error instanceof SyntaxError) {
      throw new Error('ユーザーデータのフォーマットが無効です');
    }
    // その他のエラーは再スロー
    throw error;
  } finally {
    // 成功・失敗に関わらず実行されるクリーンアップコード
    console.log(`ユーザー ${userId} の処理が終了しました`);
  }
}

// 使用例
(async () => {
  try {
    const user = await loadUserData(123);
    console.log('ユーザーをロードしました:', user);
  } catch (error) {
    console.error('ユーザーのロードに失敗しました:', error.message);
    // エラー処理(ユーザーへの通知、リトライなど)
  }
})();

5. グローバルなエラーハンドリング

5.1 未捕捉の例外 (Uncaught Exceptions)

予期せぬエラーに対しては、uncaughtException イベントをリッスンして、プロセスを終了する前にクリーンアップを実行できます。

例:グローバルエラーハンドラー

// 未捕捉の例外(同期エラー)をハンドリング
process.on('uncaughtException', (error) => {
  console.error('未捕捉の例外 (UNCAUGHT EXCEPTION) 発生! シャットダウンします...');
  console.error(error.name, error.message);

  // クリーンアップの実行(データベース接続の切断など)
  server.close(() => {
    console.log('未捕捉の例外によりプロセスを終了しました');
    process.exit(1); // 異常終了
  });
});

// ハンドルされていないプロミス拒否 (Unhandled Rejection) をハンドリング
process.on('unhandledRejection', (reason, promise) => {
  console.error('ハンドルされていない拒否 (UNHANDLED REJECTION) 発生! シャットダウンします...');
  console.error('場所:', promise, '理由:', reason);

  // サーバーを閉じて終了
  server.close(() => {
    process.exit(1);
  });
});

// ハンドルされていないプロミス拒否の例
Promise.reject(new Error('何かが失敗しました'));

// 未捕捉の例外の例
setTimeout(() => {
  throw new Error('タイムアウト後に発生した未捕捉の例外');
}, 1000);

6. エラーハンドリングのベストプラクティス

6.1 推奨事項(Do)と避けるべき事項(Don't)

カテゴリ推奨事項 (Do)避けるべき事項 (Don't)
レベル適切なレベルでエラーを処理するエラーを無視する(空の catch ブロック)
ロギング十分なコンテキストと共にエラーをログ出力する機密性の高いエラー詳細をクライアントに公開する
カスタムシナリオごとにカスタムエラータイプを使用する制御フローのために try/catch を使用する
クリーンアップfinally ブロックでリソースをクリーンアップするログ出力せずにエラーを握りつぶす
検証入力値を検証してエラーを早期にキャッチする回復不可能なエラーの後も実行を継続する

7. カスタムエラータイプ

独自のクラス(Class)を作成することで、エラーの分類と処理がより明確になります。

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.statusCode = 400; // 不正なリクエスト
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} が見つかりません`);
    this.name = 'NotFoundError';
    this.statusCode = 404; // 未検出
  }
}

// 使用例
function getUser(id) {
  if (!id) {
    throw new ValidationError('ユーザーIDは必須です', 'id');
  }
  // ...処理
}

8. まとめ

効果的なエラーハンドリングは、堅牢な Node.js アプリケーションを構築する上で不可欠な要素です。
異なるエラーの種類を理解し、適切なパターンを使用し、ベストプラクティスに従うことで、より安定し、メンテナンス性が高く、ユーザーフレンドリーなアプリケーションを作成できます。
優れたエラーハンドリングとは、単にクラッシュを防ぐことだけではありません。意味のあるフィードバックを提供し、データの整合性を維持し、問題が発生した際にも良好なユーザーエクスペリエンス(UX)を保証することなのです。