エラー処理はアプリケーションの安定性を確保するために重要な要素です。
TypeScriptを活用することで、エラー処理を型安全に行い、コードの信頼性をさらに高めることが可能です。
本記事では、高度なエラー処理と型安全性の実現方法について具体的な例とともに解説します。
なお、エラー処理と型安全性の基礎については、こちらの記事をご参照ください。
目次
カスタムエラー型の実践的な活用例
複雑なアプリケーションでは、異なる種類のエラーを扱う必要があります。
そのため、エラー階層を設計して特定のケースに対応することが有効です。
次のサンプルプログラムでは、継承(拡張)を使用して、カスタムエラー型の階層を形成しています。
TypeScript
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = "AppError";
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} が見つかりません`);
this.name = "NotFoundError";
}
}
class ValidationError extends AppError {
constructor(field: string, message: string) {
super(`フィールド "${field}": ${message}`);
this.name = "ValidationError";
}
}
コード説明
- AppError を基底クラスとして使用し、一般的なエラーの基本構造を定義します。
- NotFoundErrorやValidationErrorといった派生クラスを作ることで、特定のエラーに対応した型を設計できます。
- 各エラークラスには固有の名前とメッセージを付与し、トラブルシューティングが容易になります。
型安全なエラー処理の戦略
Result型パターン
RustやKotlinにインスパイアされたResult型をTypeScriptで実装することで、エラー処理を型安全に行えます。
TypeScript
type Result<T, E> = { success: true; value: T } | { success: false; error: E };
function validateInput(input: string): Result<string, string> {
if (input.trim() === "") {
return { success: false, error: "入力が空です" };
}
return { success: true, value: input };
}
const result = validateInput(" ");
if (result.success) {
console.log("成功:", result.value);
} else {
console.error("エラー:", result.error);
}
コード説明
- 成功時には success: true とvalue: T を返し、失敗時には success: false と error: E を返します。
- 上記の場合では、valueのTにstring、errorのEにstringを指定しており、型システムを利用して成功/失敗の状態を明示的に表現できます。
- この構造により、コードの意図がわかりやすくなり、エラー処理の安全性が向上します。
非同期処理における高度なエラーハンドリング
再試行(Retry)ロジック
外部APIの呼び出しなどで失敗が許容される場合、再試行ロジックを実装することで安定性を向上させることができます。
TypeScript
async function retry<T>(fn: () => Promise<T>, retries: number): Promise<T> {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
console.warn(`再試行 ${i + 1} 回目`);
}
}
throw lastError;
}
async function fetchData() {
// 外部APIのデータを取得するダミー関数
throw new Error("APIエラー");
}
retry(fetchData, 3).catch((error) => console.error("最終エラー:", error));
コード説明
- retry 関数は非同期処理を試行し、指定した回数だけリトライします。
- 成功した場合は結果を返し、失敗した場合は最後のエラーをスローします。
- 実際の環境では、ネットワーク接続や外部API呼び出しなどに応用可能です。
エラー集約
TypeScript
async function fetchData1() { throw new Error("API1エラー"); }
async function fetchData2() { throw new Error("API2エラー"); }
async function runAll() {
const results = await Promise.allSettled([fetchData1(), fetchData2()]);
results.forEach((result, index) => {
if (result.status === "rejected") {
console.error(`タスク ${index + 1} が失敗:`, result.reason);
}
});
}
runAll();
コード説明
- Promise.allSettled を使用して、複数の非同期処理の結果を同時に管理します。
- 成功した処理と失敗した処理を個別にハンドリングできるため、柔軟性が高まります。
非同期のエラー境界(Error Boundaries)の実装
Reactなどのフロントエンドにおけるエラー境界のコンセプトを、TypeScriptを活用して汎用的な場面に適用します。
TypeScript
function withErrorBoundary<T>(fn: () => T): T | null {
try {
return fn();
} catch (error) {
console.error("境界エラー:", error);
return null;
}
}
const safeResult = withErrorBoundary(() => {
throw new Error("予期しないエラー");
});
コード説明
- withErrorBoundary は、任意の関数を実行しつつエラーをキャッチします。
- エラー発生時はログを記録し、安全な値(ここでは null)を返すことで処理を中断します。
実プロジェクトでの応用
ライブラリのエラー型対応
外部ライブラリの型定義を活用してエラー処理を適切に行う方法を解説します。
例えば、axios を使用したHTTPリクエストのエラーハンドリングであれば、以下のようになります。
TypeScript
import axios from "axios";
async function getData() {
try {
const response = await axios.get("https://api.example.com/data");
console.log(response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Axiosエラー:", error.message);
} else {
console.error("不明なエラー:", error);
}
}
}
コード説明
- axios.get:
- 指定したURLに対してHTTP GETリクエストを送信します。この例ではURL「https://api.example.com/data」にアクセスしています。
- response.data:
- サーバーから取得したレスポンスの内容を出力します。response は axios によって提供されるレスポンスオブジェクトです。
- axios.isAxiosError:
- error が axios 固有のエラーであるかどうかを判定します。この関数は、axios が提供するユーティリティ関数で、型安全にエラーを処理できます。
- エラーハンドリング:
- axios 特有のエラー(例えばネットワークエラーやHTTPステータスコードエラー)をキャッチして処理します。
- axios 以外のエラー(例えばプログラム上のバグなど)は別途処理します。

コメント