例外処理
ClaudeCodeはエラーメッセージとスタックトレースから問題の原因を特定し、適切な修正を生成できます。弊社では、Result型パターンによる型安全なエラーハンドリングを採用することで、ClaudeCodeがエラーの種類を理解して適切な処理を実装できるようにしています。
弊社の推奨ルール
- Result型パターンを推奨 -
T | Errorの合併型を返し、IF文でエラーハンドリングする - カスタムエラークラスの活用 - 標準Errorを継承した独自のエラー型を定義する
- try-catchは最小限に - 処理をネストさせず、専用関数に切り出す
- エラーは握りつぶさない - 適切にログ出力またはユーザーに通知する
- 非同期エラーは個別に処理 - Promise.allSettledで並列処理のエラーを制御
ClaudeCodeでの利用
エラー処理の依頼例
「この関数をResult型パターンに変更して。
エラーはErrorオブジェクトで返すように」
「カスタムエラークラスを作成して。
HTTPエラー、バリデーションエラー、DBエラーを区別できるように」
「try-catchをResult型パターンに置き換えて」
リトライ処理の依頼
「このAPI呼び出しに指数バックオフでリトライ処理を追加」
「Promise.allSettledを使って並列処理のエラーを個別に処理」
標準Errorではなくカスタムエラーを使う
エラーの種類を型レベルで判別するため、具体的なエラークラスを定義します。
// ❌ 避ける:標準Errorだけを使用
function validateEmail(email: string) {
if (!email.includes('@')) {
throw new Error('Invalid email'); // エラーの種類が不明
}
}
カスタムエラークラスを定義することで、エラーの種類を型レベルで判別できます。各エラーには固有のプロパティを追加して詳細情報を保持します。
// ✅ 推奨:カスタムエラークラス
export class ValidationError extends Error {
constructor(
message: string,
public readonly field?: string // エラーが発生したフィールド名
) {
super(message)
this.name = 'ValidationError'
Object.freeze(this)
}
}
HTTPエラー用のクラスも同様に定義し、ステータスコードを含めます。
export class HttpError extends Error {
constructor(
public readonly status: number, // HTTPステータスコード
message: string
) {
super(message)
this.name = 'HttpError'
Object.freeze(this)
}
}
// 型で判別可能
function validateEmail(email: string) {
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
}
throwではなくResult型を使う
型安全なエラーハンドリングのため、T | Error の合併型で結果を返します。try-catchのcatchブロックはunknown型になり型安全ではないため、バックエンド処理では基本的にcatchしてT | Errorに変換することで型情報を強力にできます。
// ❌ 避ける:throwでエラーを隠蔽
export async function getUser(id: string): Promise<User> {
if (!id) {
throw new Error('ID required'); // 呼び出し側でキャッチし忘れのリスク
}
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error('Failed to fetch'); // エラーが握りつぶされる可能性
}
return response.json()
}
Result型パターンではT | Errorの合併型を返し、エラーの可能性を型で表現します。呼び出し側でエラーハンドリングが強制されるため、エラーの見落としを防げます。
// ✅ 推奨:Result型パターン
export async function getUser(id: string): Promise<User | Error> {
// バリデーション
if (!id) {
return new ValidationError('IDが指定されていません')
}
// データ取得処理
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return new HttpError(response.status, 'ユーザーの取得に失敗しました')
}
const data = await response.json()
return new User(data)
}
呼び出し側ではinstanceofでエラーチェックを行い、型安全にエラーハンドリングできます。
// 呼び出し側でのエラーハンドリング
const result = await getUser(userId)
// IF文でエラーを検出
if (result instanceof Error) {
console.error('エラーが発生しました:', result.message)
return
}
// ここではresultはUser型として扱える(型安全)
console.log('ユーザー名:', result.name)
ネストしたtry-catchではなく専用関数を使う
複雑な処理は専用関数に切り出して、エラーハンドリングを単純化します。
// ❌ 避ける:ネストしたtry-catch
export async function processData(input: string) {
try {
const parsed = JSON.parse(input)
try {
const validated = validateData(parsed)
try {
const result = await saveData(validated)
return result
} catch (error) {
console.error('保存エラー:', error)
throw error
}
} catch (error) {
console.error('検証エラー:', error)
throw error
}
} catch (error) {
console.error('パースエラー:', error)
throw error
}
}
ネストしたtry-catchは可読性を著しく低下させます。各処理を専用関数に分割し、Result型パターンでエラーハンドリングを統一します。
// ✅ 推奨:専用関数に切り出す
export async function parseJson(input: string): Promise<unknown | Error> {
try {
return JSON.parse(input)
} catch (error) {
return new Error('JSONのパースに失敗しました')
}
}
export async function validateData(data: unknown): Promise<ValidatedData | Error> {
if (!isValidData(data)) {
return new ValidationError('データの形式が不正です')
}
return data as ValidatedData
}
保存処理も同様に専用関数として定義し、エラーをResult型で返します。
export async function saveData(data: ValidatedData): Promise<SaveResult | Error> {
try {
const result = await database.save(data)
return result
} catch (error) {
return new Error('データの保存に失敗しました')
}
}
メイン処理は各専用関数を順次呼び出し、エラーチェックを行います。非常にシンプルで読みやすい構造になります。
// メイン処理(シンプルで読みやすい)
export async function processData(input: string): Promise<SaveResult | Error> {
const parsed = await parseJson(input)
if (parsed instanceof Error) return parsed
const validated = await validateData(parsed)
if (validated instanceof Error) return validated
const result = await saveData(validated)
return result
}
ReactでのErrorBoundaryの活用
Reactアプリケーションでは、ErrorBoundaryを使用してthrowされたエラーをキャッチし、エラーの種類に応じた適切な画面を表示できます。フロントエンドではthrowを活用してUI層でエラーハンドリングを行います。
// カスタムエラーの定義
export class AuthError extends Error {
constructor(message: string) {
super(message)
this.name = 'AuthError'
Object.freeze(this)
}
}
export class NetworkError extends Error {
constructor(message: string) {
super(message)
this.name = 'NetworkError'
Object.freeze(this)
}
}
ReactのErrorBoundaryでは、エラーの種類に応じて適切な画面遷移やUI表示を行います。componentDidCatchでエラーをキャッチし、instanceof演算子で種類を判別します。
// ErrorBoundaryでエラーをキャッチ
class AppErrorBoundary extends React.Component {
componentDidCatch(error: Error) {
// エラーの種類に応じて画面遷移
if (error instanceof AuthError) {
// ログイン画面を表示
window.location.href = '/login'
} else if (error instanceof NetworkError) {
// ネットワークエラー画面を表示
this.setState({ showNetworkError: true })
}
}
render() {
if (this.state.hasError) {
return <ErrorFallback />
}
return this.props.children
}
}
コンポーネント内では、非同期処理でエラーが発生した際にthrowでErrorBoundaryに伝播させます。
// コンポーネント内でthrow
function UserProfile() {
useEffect(() => {
fetchUserData()
.catch(error => {
if (error.status === 401) {
throw new AuthError('認証が必要です')
}
throw new NetworkError('ネットワークエラー')
})
}, [])
return <div>ユーザープロフィール</div>
}
このパターンにより、UI層で適切なエラー画面やログイン画面への誘導が実現できます。
Effectライブラリという選択肢について
Effectは非常に強力なTypeScriptライブラリで、エラーハンドリング、リトライ、タイムアウト、並列実行、リソース管理などを統一的に扱えます。機能的にはEffectの方が有用で価値がありますが、外部ライブラリへの依存を避けたいのと実装が複雑になるため、弊社では採用していません。
// Effectライブラリの基本的な使用例
import { Effect } from "effect"
// 基本的な処理
function fetchUser(id: string): Effect.Effect<User, HttpError> {
return Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: () => new HttpError("ユーザー取得に失敗")
})
}
Effectの最大の特徴は、複雑な非同期処理を宣言的に記述できることです。リトライやタイムアウトも簡潔に表現できます。
// 自動リトライとタイムアウトの例
const userWithRetry = fetchUser("1").pipe(
Effect.retry(Schedule.exponential("1 second")),
Effect.timeout("10 seconds")
)
また、依存性注入とエラーハンドリングも統一的に扱えます。
// エラーハンドリングと実行
const runnable = userWithRetry.pipe(
Effect.catchTags({
HttpError: (error) => Effect.logError(`HTTP: ${error.message}`)
})
)
Effect.runPromise(runnable)
Effectを使用すると、複雑な非同期処理、エラーハンドリング、リソース管理が宣言的かつ型安全に記述できます。特に大規模アプリケーションでは、その価値は非常に高くなります。しかし、弊社ではライブラリ依存を最小限に抑え、シンプルなT | Errorパターンで運用しています。
よくある問題
throwとResult型の使い分け
いつthrowを使い、いつResult型を使うべきか分からない問題です。回復不可能なプログラミングエラーはthrow、回復可能なビジネスエラーはResult型を使うことで解決できます。
// ✅ 回復不可能なエラーはthrow
export function invariant(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(`Invariant violation: ${message}`)
}
}
// ✅ 回復可能なエラーはResult型
export function divide(a: number, b: number): number | Error {
if (b === 0) {
return new Error('ゼロ除算はできません') // 呼び出し側で対処可能
}
return a / b
}
try-catchの型安全性の問題
catchブロックでキャッチされるエラーはunknown型になるため、型安全ではありません。バックエンドではcatchしてResult型に変換することで解決できます。
// ❌ catchブロックは型安全ではない
try {
const result = await riskyOperation()
return result
} catch (error) {
// errorはunknown型、型安全ではない
console.error(error.message) // エラー:unknown型にmessageプロパティはない
}
catchブロックのerrorは常にunknown型になるため、型安全ではありません。バックエンドではcatchしてResult型に変換することで型安全性を保ちます。
// ✅ バックエンド:catchしてResult型に変換
async function safeRiskyOperation(): Promise<Result | Error> {
try {
const result = await riskyOperation()
return result
} catch (error) {
// errorを適切なError型に変換
if (error instanceof Error) {
return error
}
return new Error('予期しないエラーが発生しました')
}
}
async関数でのエラーがキャッチできない
Promiseチェーンでcatchを忘れて未処理のPromise rejectionが発生する問題です。必ずcatchメソッドでエラーハンドリングを行うことで解決できます。
// ❌ catchを忘れる
getData()
.then(result => {
if (result instanceof Error) return;
processData(result);
}); // 予期しないエラーが握りつぶされる
// ✅ catchで予期しないエラーもハンドリング
getData()
.then(result => {
if (result instanceof Error) {
handleError(result);
return;
}
processData(result);
})
.catch(error => {
console.error('予期しないエラー:', error);
});