Interactive

例外処理

ClaudeCodeはエラーメッセージとスタックトレースから問題の原因を特定し、適切な修正を生成できます。弊社では、Result型パターンによる型安全なエラーハンドリングを採用することで、ClaudeCodeがエラーの種類を理解して適切な処理を実装できるようにしています。

弊社の推奨ルール

  1. Result型パターンを推奨 - T | Error の合併型を返し、IF文でエラーハンドリングする
  2. カスタムエラークラスの活用 - 標準Errorを継承した独自のエラー型を定義する
  3. try-catchは最小限に - 処理をネストさせず、専用関数に切り出す
  4. エラーは握りつぶさない - 適切にログ出力またはユーザーに通知する
  5. 非同期エラーは個別に処理 - 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);
  });