🐕
React+TypeScript fetchでの非同期処理とエラーハンドリング
はじめに
React + TypeScriptでAPIを呼び出す際、エラーハンドリングは避けて通れません。しかし、「とりあえずtry-catchで囲む」だけでは不十分です。この記事では、実践的なエラーハンドリングパターンを解説します。
基本的なtry-catch-finallyパターン
typescriptconst handleNameSearch = async (e: React.FormEvent) => {
e.preventDefault();
onLoadingChange(true); // ローディング開始
try {
const response = await fetch('http://localhost:3000/api/v1/wines/search_by_name', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: searchQuery }),
});
if (!response.ok) {
throw new Error('検索に失敗しました');
}
const data: WineResponse = await response.json();
onResult(data);
} catch (error) {
onError(error instanceof Error ? error.message : '検索エラーが発生しました');
} finally {
onLoadingChange(false); // 必ずローディング終了
}
};
このパターンの重要なポイントは3つです。
1. finallyでローディング状態を確実に解除
finallyブロックは、成功・失敗に関わらず必ず実行されます。
typescripttry {
onLoadingChange(true);
// API呼び出し
} catch (error) {
// エラー処理
} finally {
onLoadingChange(false); // 成功でも失敗でも必ず実行
}
もしfinallyがないと:
onLoadingChange(true);
try {
const response = await fetch(...);
onLoadingChange(false); // 成功時のみ実行
} catch (error) {
onLoadingChange(false); // エラー時にも書く必要がある
}
同じコードを2箇所に書くことになり、保守性が下がります。
2. response.okのチェック
fetchはネットワークエラー以外ではエラーをthrowしません。HTTPステータスコード400や500でも、catchブロックには入りません。
typescriptconst response = await fetch(...);
// fetchは404や500でもエラーをthrowしない!
// 明示的にチェックが必要
if (!response.ok) {
throw new Error('検索に失敗しました');
}
レスポンスステータスに応じたエラー処理:
typescriptif (!response.ok) {
switch (response.status) {
case 400:
throw new Error('リクエストが不正です');
case 404:
throw new Error('ワインが見つかりませんでした');
case 500:
throw new Error('サーバーエラーが発生しました');
default:
throw new Error(`エラーが発生しました (${response.status})`);
}
}
3. 型ガードでエラーの型を安全に扱う
JavaScriptでは、catchでキャッチされるエラーはunknown型です。TypeScriptでは型安全に扱う必要があります。
typescriptcatch (error) {
// errorはunknown型
onError(error instanceof Error ? error.message : '検索エラーが発生しました');
}
instanceof Errorで型ガードを行い、Errorオブジェクトならmessageプロパティにアクセスできます。
より実践的なパターン
- パターン1: カスタムエラークラス
typescriptclass ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public details?: unknown
) {
super(message);
this.name = 'ApiError';
}
}
const handleApiRequest = async () => {
try {
const response = await fetch(...);
if (!response.ok) {
const errorData = await response.json();
throw new ApiError(
errorData.message || 'リクエストに失敗しました',
response.status,
errorData
);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.statusCode}:`, error.message);
// ステータスコードに応じた処理
} else if (error instanceof Error) {
console.error('Network Error:', error.message);
}
throw error;
}
};
- パターン2: エラーメッセージの一元管理
typescriptconst ERROR_MESSAGES = {
NETWORK: 'ネットワークエラーが発生しました',
TIMEOUT: 'リクエストがタイムアウトしました',
SERVER: 'サーバーエラーが発生しました',
NOT_FOUND: 'データが見つかりませんでした',
VALIDATION: '入力内容に誤りがあります',
} as const;
const handleError = (error: unknown): string => {
if (error instanceof TypeError) {
return ERROR_MESSAGES.NETWORK;
}
if (error instanceof Error) {
if (error.message.includes('timeout')) {
return ERROR_MESSAGES.TIMEOUT;
}
return error.message;
}
return '予期しないエラーが発生しました';
};
// 使用例
catch (error) {
const message = handleError(error);
onError(message);
}
- パターン3: リトライ機能付きfetch
typescriptconst fetchWithRetry = async (
url: string,
options: RequestInit,
retries = 3
): Promise<Response> => {
try {
const response = await fetch(url, options);
if (!response.ok && response.status >= 500 && retries > 0) {
// サーバーエラーの場合はリトライ
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(url, options, retries - 1);
}
return response;
} catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(url, options, retries - 1);
}
throw error;
}
};
// 使用例
try {
const response = await fetchWithRetry('http://localhost:3000/api/v1/wines', {
method: 'POST',
body: JSON.stringify(data),
});
const result = await response.json();
} catch (error) {
onError('リトライ後もエラーが発生しました');
}
- パターン4: タイムアウト処理
typescriptconst fetchWithTimeout = async (
url: string,
options: RequestInit,
timeout = 5000
): Promise<Response> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('リクエストがタイムアウトしました');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
};
まとめ
TypeScriptでの非同期エラーハンドリングのポイント:
-finallyでクリーンアップ処理を確実に実行
-response.okで明示的にHTTPエラーをチェック
-型ガード(instanceof)で型安全にエラーを扱う
-カスタムエラークラスで詳細な情報を保持
-エラーメッセージは一元管理して保守性を高める
Discussion