TypeScript エラーハンドリング
信頼性の高い TypeScript アプリケーションを構築する上で、堅牢なエラーハンドリングは極めて重要です。
本ガイドでは、基本的な try/catch から、高度なエラーハンドリングパターンまでを網羅的に解説します。
1. エラーハンドリングの基礎 (Try/Catch ブロック)
エラーハンドリングの最も基本的な構成要素は以下の通りです。
コード例
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('0で割ることはできません');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
// エラーメッセージの出力
console.error('エラーが発生しました:', error.message);
}1.1 TypeScript 4.0 以降の注意点
TypeScript 4.0 以降では、catch ブロックの変数はデフォルトで unknown 型になります。プロパティにアクセスする前に、必ず型を絞り込む(Narrowing)必要があります。
2. カスタムエラークラス (Custom Error Classes)
2.1 カスタムエラークラスの作成
組み込みの Error クラスを継承して、ドメイン固有のエラーを作成します。
コード例
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
// プロトタイプチェーンの復元(TSで重要)
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// 使用例
function validateUser(user: any) {
if (!user.name) {
throw new ValidationError('名前は必須です', 'name');
}
if (!user.email.includes('@')) {
throw new ValidationError('メールアドレスの形式が正しくありません', 'email');
}
}3. エラーのための型ガード (Type Guards for Errors)
3.1 エラーハンドリングのための型述語
異なるエラー型を安全に扱うために、型ガード(Type Guards)を作成します。
コード例
// 型述語(Type Predicates)
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
// catch ブロックでの使用
try {
validateUser({});
} catch (error: unknown) {
if (isValidationError(error)) {
console.error(`バリデーションエラー(フィールド: ${error.field}): ${error.message}`);
} else if (isErrorWithMessage(error)) {
console.error('エラーが発生しました:', error.message);
} else {
console.error('不明なエラーが発生しました');
}
}3.2 型アサーションパターン
より複雑なエラーハンドリングでは、型アサーション関数を使用することを検討してください。
コード例
function assertIsError(error: unknown): asserts error is Error {
if (!(error instanceof Error)) {
throw new Error('キャッチされた値は Error インスタンスではありません');
}
}
try {
// ... 処理
} catch (error) {
assertIsError(error);
// TypeScript はここで error が Error インスタンスであることを認識します
console.error((error as Error).message);
}4. 非同期エラーハンドリング (Async Error Handling)
4.1 Async/Await エラーのハンドリング
async/await コードにおける適切なエラーハンドリングには、await 呼び出しを try/catch ブロックでラップする必要があります。
コード例
interface User {
id: number;
name: string;
email: string;
}
// async/await と try/catch の併用
async function fetchUser(userId: number): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTPエラー! ステータス: ${response.status}`);
}
return await response.json() as User;
} catch (error) {
if (error instanceof Error) {
console.error('ユーザーの取得に失敗しました:', error.message);
}
throw error; // 呼び出し元が処理できるように再スロー
}
}
// Promise.catch() を使用したエラーハンドリング
function fetchUserPosts(userId: number): Promise<any[]> {
return fetch(`/api/users/${userId}/posts`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTPエラー! ステータス: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('投稿の取得に失敗しました:', error);
return []; // フォールバックとして空配列を返す
});
}4.2 未処理の Promise リジェクション
警告を避けるため、必ず Promise のリジェクション(拒否)を処理してください。
コード例
// NG: 未処理の Promise リジェクション
fetchData().then(data => console.log(data));
// OK: 成功とエラーの両方を処理
fetchData()
.then(data => console.log('成功:', data))
.catch(error => console.error('エラー:', error));
// または、意図的にエラーを無視する場合は void を使用
void fetchData().catch(console.error);5. React における Error Boundary
5.1 React Error Boundary コンポーネント
React のコンポーネントツリー内で JavaScript エラーをキャッチするための Error Boundary(エラー境界)を作成します。
コード例
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = {
hasError: false
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('キャッチされなかったエラー:', error, errorInfo);
// エラーレポートサービスへのログ出力など
}
public render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>問題が発生しました</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
再試行する
</button>
</div>
);
}
return this.props.children;
}
}
// 使用例
function App() {
return (
<ErrorBoundary fallback={<div>おっと!何かが壊れました。</div>}>
<MyComponent />
</ErrorBoundary>
);
}6. ベストプラクティス (Best Practices)
6.1 必ずエラーを処理する
catch ブロックを空のままにしないでください。最低限、エラーをログに記録しましょう。
コード例
// NG: サイレントエラー(黙殺)
try { /* ... */ } catch { /* 空 */ }
// OK: 少なくともエラーをログ出力する
try { /* ... */ } catch (error) {
console.error('操作に失敗しました:', error);
}6.2 具体的なエラー型を使用する
異なるエラーシナリオに合わせて、カスタムエラークラスを作成します。
コード例
class NetworkError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}6.3 適切なレベルでエラーを処理する
回復(リカバリ)が可能であったり、ユーザーに適切なフィードバックを提供できるコンテキストを持っている場所でエラーを処理します。
コード例
// データアクセスレイヤー
async function getUser(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NetworkError(response.status, 'ユーザーの取得に失敗しました');
}
return response.json();
}
// UIコンポーネント
async function loadUser() {
try {
const user = await getUser('123');
setUser(user);
} catch (error) {
if (error instanceof NetworkError) {
if (error.status === 404) {
showError('ユーザーが見つかりません');
} else {
showError('ネットワークエラーが発生しました。後でもう一度お試しください。');
}
} else {
showError('予期せぬエラーが発生しました');
}
}
}7. よくある落とし穴 (Common Pitfalls)
7.1 Promise のリジェクションを無視する
未処理のリジェクション警告を防ぐため、必ず Promise の失敗をハンドリングしてください。
7.2 型の絞り込みをせずに catch する
TypeScript 4.0 以降、キャッチされたエラーは unknown 型になります。
コード例
// NG: エラーが 'unknown' 型
try { /* ... */ } catch (error) {
console.log(error.message); // エラー: 'unknown' 型にプロパティ 'message' は存在しません
}
// OK: 型を絞り込む
try { /* ... */ } catch (error) {
if (error instanceof Error) {
console.log(error.message); // OK
}
}7.3 エラーの飲み込み(Swallowing Errors)
適切な処理を行わずに、エラーを黙ってキャッチして無視することは避けてください。
コード例
// NG: エラーがサイレントに無視される
function saveData(data: Data) {
try {
database.save(data);
} catch {
// 無視
}
}
// より良い方法: エラーをログに記録し、ユーザーに通知する
function saveData(data: Data) {
try {
database.save(data);
} catch (error) {
console.error('データの保存に失敗しました:', error);
showError('データの保存に失敗しました。もう一度お試しください。');
}
}8. まとめ (Summary)
TypeScript における効果的なエラーハンドリングのポイントは以下の通りです:
- 同期コードには
try/catchブロックを使用する - 非同期コードには
.catch()またはasync/await+try/catchを使用する - ドメイン固有のエラーには カスタムエラークラス を作成する
- 型ガード を使用して、エラーオブジェクトを安全に操作する
- アプリケーション内の 適切な階層 でエラーを処理する
- ユーザーに 意味のあるエラーメッセージ を提供する
これらのプラクティスに従うことで、より堅牢でメンテナンス性の高い TypeScript アプリケーションを構築することができます。