概要
TypeScriptにおけるエラーハンドリングとして、カスタムエラーの活用とエラー処理の集約化について解説します。関数で一元的にエラー処理を行うことで、コードの冗長さや重複を排除します。これにより、可読性と再利用性、保守性を大幅に向上させ、効率的かつ効果的なエラーハンドリングを実現する方法を具体的なコード例とともに紹介します。
カスタムエラーでエラー判別を明確に
エラーを適切に判別し、それぞれのエラーに適切な処理を行えるようにするための一つの効果的な方法として、カスタムエラーを使用します。カスタムエラーを使用すると、エラーの種類ごとに固有のクラスを定義できるため、instanceof
演算子などを用いてエラーの種類を正確に特定することが可能になります。これによって、UserIdInvalidError
のように、「何が原因で発生したエラーなのか」を明確にでき、エラーの内容に応じた適切な処理(エラーメッセージの出し分けや、リトライ処理など)を記述しやすくなるのです。
// カスタムエラークラスの例
class UserIdInvalidError extends Error {
constructor(message: string = "Invalid User ID") {
super(message);
this.name = "UserIdInvalidError";
}
}
// バリデーションスキーマ (Zod)
const userIdSchema = z.string();
// バリデーション関数
const validateUserId = (userId: unknown) => {
try {
return userIdSchema.parse(userId);
} catch (error) {
throw new UserIdInvalidError("User ID Validation Error");
}
};
// controller の例
const getResult = async (req: any, res: any) => {
try {
const userId = validateUserId(req.body.userId);
// DB からデータを取得
const result = await getResultById(userId);
res.status(200).json(result);
} catch (error) {
if (error instanceof UserIdInvalidError) {
res.status(401).json({ error: error.message });
} else if (error instanceof MongoError) {
res.status(500).json({ error: "DATABASE_ERROR" });
} else {
res.status(500).json({ error: "INTERNAL_SERVER_ERROR" });
}
}
};
この例では、UserIdInvalidError というカスタムエラーを定義し、validateUserId 関数では、入力されたuserIdが不正な場合にこのカスタムエラーをスローしています。
しかし、カスタムエラーの種類が増えてくると、エラーハンドリングのコードが冗長になり、可読性や保守性が低下してしまう可能性があります。
冗長なエラーハンドリングの例 (カスタムエラーが増えた場合)
validateUserIdはユーザーIDを、validateResultIdはリザルトIDを、validateResultはDBから取ってきたデータをそれぞれZodでバリデーションする関数です。
const getResult = async (req: any, res: any) => {
try {
const userId = validateUserId(req.userId); //エラー時、UserIdSchemaErrorをスロー
const resultId = validateResultId(req.params.resultId); // ResultIdSchemeErrorをスロー
const result = await getResultById(resultId, userId); // ResultNotFoundError,MongoErrorをスロー
const validatedResult = validateResult(result); //ResultSchemaErrorをスロー
res.status(200).json(validatedResult);
} catch (error) {
if (error instanceof UserIdSchemaError) {
res.status(401).json({ error: "USER_ID_INVALID" });
} else if (error instanceof ResultIdSchemeError) {
res.status(400).json({ error: "RESULT_ID_INVALID" });
} else if (error instanceof ResultNotFoundError) {
res.status(404).json({ error: "RESULT_NOT_FOUND" });
} else if (error instanceof ResultSchemaError) {
res.status(404).json({ error: "RESULT_SCHEMA_ERROR" });
} else if (error instanceof MongoError) {
res.status(500).json({ error: "DATABASE_ERROR" });
} else {
res.status(500).json({ error: "INTERNAL_SERVER_ERROR" });
}
}
};
このコードでは、try...catchブロック内で発生する可能性のある各エラーに対して、if...else ifの連鎖を用いて個別に処理しています。この方法は、エラーの種類が増えるにつれてコードが肥大化し、可読性や保守性が損なわれる原因となります。また、全てのcontrollerで同様の問題が起きかねません。
エラーハンドリングを集約化して、再利用性と可読性を高める!
上記の冗長なエラーハンドリングを改善するためには、エラー処理を一箇所に集約し、汎用的な関数で処理することが効果的です。これにより、コードの重複を排除し、再利用性のしやすさと可読性、保守性を大幅に向上させることができます。以下はその一例です。
// errorHandlers.ts
export const handleErrors = (error: unknown) => {
if (error instanceof UserIdSchemaError) {
return { statusCode: 401, body: { error: "USER_ID_INVALID" } };
} else if (error instanceof ResultIdSchemaError) {
return { statusCode: 400, body: { error: "RESULT_ID_INVALID" } };
} else if (error instanceof ResultSchemaError) {
return { statusCode: 400, body: { error: "RESULT_SCHEMA_ERROR" } };
} else if (error instanceof ResultNotFoundError) {
return { statusCode: 404, body: { error: "RESULT_NOT_FOUND" } };
} else if (error instanceof MongoError) {
return { statusCode: 500, body: { error: "DATABASE_ERROR" } };
} else if (error instanceof Error) {
return { statusCode: 500, body: { error: "INTERNAL_SERVER_ERROR" } };
} else {
return { statusCode: 500, body: { error: "INTERNAL_SERVER_ERROR" } };
}
//その他getResult以外のcontrollerで発生しうるカスタムエラーもここにまとめて書く
};
// getResult.ts
const getResult = async (req: any, res: any) => {
try {
const userId = validateUserId(req.userId);
const resultId = validateResultId(req.params.resultId);
const result = await getResultById(resultId, userId);
const validatedResult = validateResult(result);
res.status(200).json(result);
} catch (error) {
const { statusCode, body } = handleErrors(error);
res.status(statusCode).json(body);
}
};
この改善されたコードでは、以下の点が変更されています。
-
エラーハンドリング関数の集約:
-
handleErrors
という専用の関数で、エラーの種類に応じて適切なステータスコードとエラーメッセージを生成しています。 - この
handleErrors
関数に全てのエラーパターンを集約させておけば、それぞれのcontroller
のエラーハンドリング部分で再利用できます。
-
-
getResult
関数の簡素化:-
try...catch
ブロック内のエラー処理がhandleErrors
関数の呼び出しのみになり、コードが大幅に簡素化され、可読性が向上しています。
-
ポイント
-
handleErrors
関数は、error
のみを受け取り、エラーの種類に応じたステータスコードとエラー情報のみを返すようにしましょう。res
までhandleErrors
関数に渡してしまうと、この関数がHTTPレスポンスに関与する(=Expressに依存する)ことになり、関心の分離が崩れてしまいます。handleErrors
は純粋なエラー処理ロジックに専念させましょう。 - 全てのエラーパターンを
handleErrors
関数に集約させておくことで、各コントローラーのエラーハンドリング部分でこの関数を再利用でき、コードの重複を避けられます。
まとめ
本記事では、TypeScriptにおけるエラーハンドリングのとして、カスタムエラーの活用とエラー処理の集約化について解説しました。また、 handleErrors
関数をもう少し簡易的に書くことはできますが、今回は割愛したいと思います。開発の参考になれば幸いです。