[Node.js, Typescript] 初学者のためのエラーハンドリング
概要
本記事では、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
オブジェクトでは、message
や stack
などの主要プロパティは列挙不可(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でのエラーハンドリングで重要なポイントは以下の通り:
-
try-catch-finally
構文を使ってエラーをキャッチする - TypeScriptでは
catch
節のエラー変数にはunknown
型を使用し、型チェックを行って型安全性を確保する - 標準の
Error
クラスを拡張してアプリケーション固有のエラークラスを作成できる - エラーを再スローする際には元のエラー情報を保持する
- 元のエラーをそのまま再スローする
- ES2022以降の
cause
プロパティを使って元のエラーを保持する
適切なエラーハンドリングを行うことで、デバッグがしやすく、より堅牢なアプリケーションを構築できる。
Discussion