📫

[Node.js, Typescript] 初学者のためのエラーハンドリング

2025/02/27に公開

概要

本記事では、Node.jsとTypeScriptを使った開発における基本的なエラーハンドリングの方法について解説する。

try-catch-finallyについて

基本的な使い方

JavaScriptおよびTypeScriptでは、try-catch-finally構文を使ってエラーハンドリングを行う。

try {
  // エラーが発生する可能性のあるコード
  const result = riskyOperation();
} catch (error) {
  // エラーが発生した場合の処理
  console.error('エラーが発生:', error);
} finally {
  // エラーの有無に関わらず実行される処理
  console.log('処理が完了');
}

TypeScriptでの型付け

TypeScriptでは、catchブロックでのエラーオブジェクトに型を付けることができる。
(TypeScript 4.0より前のバージョンでは、catch節のエラー変数は暗黙的にany型だった)
unknown型を使用することで、エラーを適切に型チェックしてから扱うことを強制でき、より型安全なコードが書ける。

try {
  // 何らかの処理
} catch (error: unknown) {
  // unknownを使うことで型安全性を高める
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error(String(error));
  }
}

any型との違い

errorをanyとした場合、スローされたものがErrorオブジェクトでないと(例えば文字列や数値がスローされた場合)ランタイムエラーが発生する可能性があります。

try {
  // 処理
} catch (error: any) {
  console.error(error.message); // エラーオブジェクトでない場合、ランタイムエラーになる可能性がある
}

Errorクラスと拡張

標準のErrorクラス

JavaScriptには、標準でErrorクラスが用意されている。基本的なエラーオブジェクトは以下のプロパティを持つ:

  • message: エラーメッセージ
  • name: エラーの名前
  • stack: スタックトレース(実行環境による)
const error = new Error('エラーが発生');
console.log(error.message); // 'エラーが発生'
console.log(error.name);    // 'Error'

カスタムエラークラスの作成

TypeScriptでは、基本のErrorクラスを拡張して、アプリケーション固有のエラークラスを定義できる:

// 基本的なカスタムエラークラス
class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AppError';
  }
}

// より具体的なエラークラス
class DatabaseError extends AppError {
  constructor(message: string, public readonly code: string) {
    super(message);
    this.name = 'DatabaseError';
  }
}

// 使用例
try {
  throw new DatabaseError('データベース接続に失敗', 'DB_CONNECTION_FAILED');
} catch (error) {
  if (error instanceof DatabaseError) {
    console.error(`${error.name}: ${error.message} (コード: ${error.code})`);
  } else if (error instanceof AppError) {
    console.error(`${error.name}: ${error.message}`);
  } else {
    console.error('未知のエラーが発生');
  }
}

アンチパターン

エラーの再作成と再スロー

関数内でキャッチしたエラーを新しい Error オブジェクトとして作り直して再スローするのは、スタックトレース情報が失われるため避けるべき。

// アンチパターン: 元のエラーを捨てて新しいエラーを作成
function processData(data: string) {
  try {
    return validateAndTransform(data);
  } catch (error) {
    // 悪い例: 元のエラー情報が失われる
    throw new Error(`データ処理中にエラーが発生: ${error}`);
  }
}

上記のコードでは、元のエラーが持っていたスタックトレースや型情報が失われてしまう。これにより、実際にどこでエラーが発生したのかを特定することが難しくなる。

推奨パターン

エラーを再スローする場合は、以下のいずれかの方法を使用すべき:

1. 元のエラーをそのまま再スロー

function processData(data: string) {
  try {
    // ここでエラーがスローされる
    return validateAndTransform(data);
  } catch (error) {
    // エラーをログに記録するだけで、そのまま再スロー
    console.error('データ処理中にエラーが発生:', error);
    throw error; // 元のエラーをそのまま再スロー
  }
}

2. エラーをラップして情報を保持

ES2022 以降では、組み込みの Error クラスに cause プロパティが追加された。これを利用すると以下のようにシンプルに書ける:

function processData(data: string) {
 try {
   return validateAndTransform(data);
 } catch (error) {
   throw new Error('データ処理中にエラーが発生', { cause: error });
 }
}

// エラーとcauseの参照方法
try {
 processData(data);
} catch (error) {
 if (error instanceof Error) {
   console.error(error.message); // "データ処理中にエラーが発生"
   
   // cause プロパティにアクセス
   if (error.cause instanceof Error) {
     console.error('原因:', error.cause); // 元のエラー内容
   }
 }
}

列挙可能性について

エラープロパティの列挙可能性の問題

JavaScript の Error オブジェクトには、列挙可能性に関する重要な特性がある。標準の Error オブジェクトでは、messagestack などの主要プロパティは列挙不可(non-enumerable)に設定されている。

const error = new Error('エラー');

// プロパティを列挙
for (const prop in error) {
  console.log(prop); // 標準プロパティ(message, stack)は表示されない
}

console.log(Object.keys(error)); // [](空配列)

この挙動により、エラーオブジェクトをシリアライズすると問題が発生することがある:

const error = new Error('エラー');
console.log(JSON.stringify(error)); // "{}" - 空オブジェクトになる

カスタムエラークラスでの対応

カスタムエラークラスを作成する際、重要なプロパティを列挙可能にするには以下のアプローチが有効:

class CustomError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
    this.name = 'CustomError';
    
    // プロパティを列挙可能にする
    Object.defineProperty(this, 'message', { enumerable: true });
    Object.defineProperty(this, 'name', { enumerable: true });
    Object.defineProperty(this, 'code', { enumerable: true });
  }
}

const customError = new CustomError('カスタムエラー', 'ERR_CUSTOM');
console.log(JSON.stringify(customError)); // {"message":"カスタムエラー","name":"CustomError","code":"ERR_CUSTOM"}

この方法により、エラーログやデバッグにおいて重要な情報を失うことなくエラーをシリアライズできる。

AWS SDK for JavaScript v3におけるエラーの列挙可能性

AWS SDK for JavaScript v3では、エラーオブジェクトがデフォルトで列挙可能なプロパティを持っている。これにより、エラーオブジェクトのプロパティにアクセスしたり、シリアライズしてログに出力したりすることが容易になる。

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({ region: 'us-east-1000' });

async function example() {
  try {
    const command = new GetObjectCommand({ Bucket: 'nonexistent-bucket', Key: 'file.txt' });
    await s3.send(command);
  } catch (error) {
    if (error instanceof ResourceNotFoundException) {
      console.log(Object.keys(error)); // ["message", "name", "stack", "$fault", "$metadata", "Type", ...]
      console.log(JSON.stringify(error)); // シリアライズ可能
      // 例: {"message":"The specified bucket does not exist","name":"NoSuchBucket","stack":"...","statusCode":400,"requestId":"..."}
      console.log(JSON.stringify(error.stack)); // stackは別で出力する必要がある
    }
  }
}

まとめ

TypeScriptとNode.jsでのエラーハンドリングで重要なポイントは以下の通り:

  1. try-catch-finally構文を使ってエラーをキャッチする
  2. TypeScriptではcatch節のエラー変数にはunknown型を使用し、型チェックを行って型安全性を確保する
  3. 標準のErrorクラスを拡張してアプリケーション固有のエラークラスを作成できる
  4. エラーを再スローする際には元のエラー情報を保持する
    • 元のエラーをそのまま再スローする
    • ES2022以降のcauseプロパティを使って元のエラーを保持する

適切なエラーハンドリングを行うことで、デバッグがしやすく、より堅牢なアプリケーションを構築できる。

Discussion