エラーハンドリングパターン - 堅牢なアプリケーション設計

15分 read | 2025.01.10

エラーハンドリングの原則

  1. 早期に失敗する(Fail Fast) - 問題を早期に検出
  2. 明示的にする - 暗黙のエラーを避ける
  3. 回復可能性を考慮 - リトライ可能かを判断
  4. ユーザーに適切なフィードバック - 技術的詳細は隠す

エラーの分類

種類対処法
予期されるエラー入力バリデーション失敗ユーザーにフィードバック
回復可能なエラーネットワークタイムアウトリトライ
致命的エラーメモリ不足ログ出力して終了
プログラムエラーnull参照バグ修正

Result型パターン

例外を使わず、戻り値でエラーを表現します。

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

// 使用例
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      return {
        success: false,
        error: new ApiError(response.status, 'User not found'),
      };
    }

    const user = await response.json();
    return { success: true, data: user };

  } catch (error) {
    return {
      success: false,
      error: new ApiError(500, 'Network error'),
    };
  }
}

// 呼び出し側
const result = await fetchUser('123');

if (result.success) {
  console.log(result.data.name);
} else {
  console.error(result.error.message);
}

カスタムエラークラス

// ベースエラー
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// 具体的なエラー
class ValidationError extends AppError {
  constructor(message: string, public fields: Record<string, string>) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

// 使用
throw new ValidationError('Invalid input', {
  email: 'Invalid email format',
  password: 'Too short',
});

グローバルエラーハンドラー

// Express.js
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  // ログ出力
  logger.error({
    err,
    requestId: req.headers['x-request-id'],
    path: req.path,
    method: req.method,
  });

  // 運用エラー(予期されるエラー)
  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        ...(err instanceof ValidationError && { fields: err.fields }),
      },
    });
  }

  // プログラムエラー(予期しないエラー)
  // 詳細は隠す
  return res.status(500).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
    },
  });
};

app.use(errorHandler);

Reactエラー境界

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // エラー報告サービスに送信
    reportError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

リトライパターン

interface RetryConfig {
  maxAttempts: number;
  baseDelay: number;
  maxDelay: number;
  retryableErrors?: string[];
}

async function withRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // リトライ不可のエラー
      if (config.retryableErrors &&
          !config.retryableErrors.includes(error.code)) {
        throw error;
      }

      if (attempt === config.maxAttempts) break;

      // 指数バックオフ + ジッター
      const delay = Math.min(
        config.baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
        config.maxDelay
      );

      logger.warn({ attempt, delay, error: error.message }, 'Retrying...');
      await sleep(delay);
    }
  }

  throw lastError!;
}

// 使用
const data = await withRetry(
  () => fetchExternalApi(),
  { maxAttempts: 3, baseDelay: 1000, maxDelay: 10000 }
);

グレースフルデグラデーション

async function getProductWithFallback(id: string): Promise<Product> {
  try {
    // プライマリソース
    return await productService.get(id);
  } catch (error) {
    logger.warn({ id, error }, 'Primary source failed, trying cache');

    try {
      // キャッシュフォールバック
      const cached = await cache.get(`product:${id}`);
      if (cached) return cached;
    } catch (cacheError) {
      logger.warn({ id }, 'Cache also failed');
    }

    // 最終フォールバック:デフォルト値
    return getDefaultProduct(id);
  }
}

関連記事

まとめ

堅牢なエラーハンドリングは、Result型、カスタムエラークラス、グローバルハンドラー、リトライパターンを組み合わせて実現します。ユーザー体験と運用効率の両方を考慮した設計を心がけましょう。

← Back to list