エラーハンドリング
堅牢なWebアプリケーションを構築するには、適切なエラーハンドリングが不可欠です。予期しないエラーからアプリケーションを保護し、ユーザーに分かりやすいエラーメッセージを提供する方法について学んでいきましょう。
エラーハンドリングの基本概念
エラーの種類
Webアプリケーションで発生するエラーは大きく分けて以下の種類があります:
- 
クライアントエラー(4xx)
- バリデーションエラー(400 Bad Request)
 - 認証エラー(401 Unauthorized)
 - 認可エラー(403 Forbidden)
 - リソースが見つからない(404 Not Found)
 
 - 
サーバーエラー(5xx)
- 内部サーバーエラー(500 Internal Server Error)
 - データベース接続エラー
 - 外部API連携エラー
 
 
Honoでのエラーハンドリング
基本的なエラーレスポンス
import { Hono } from 'hono'
const app = new Hono()
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  // バリデーション
  if (!id || !/^\d+$/.test(id)) {
    return c.json({
      error: 'INVALID_ID',
      message: 'User ID must be a positive integer'
    }, 400)
  }
  try {
    const user = await getUserById(id)
    if (!user) {
      return c.json({
        error: 'USER_NOT_FOUND',
        message: `User with ID ${id} not found`
      }, 404)
    }
    return c.json(user)
  } catch (error) {
    console.error('Error fetching user:', error)
    return c.json({
      error: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred'
    }, 500)
  }
})
グローバルエラーハンドラーの実装
import { Hono } from 'hono'
class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: any
  ) {
    super(message)
    this.name = 'AppError'
  }
}
const app = new Hono()
// グローバルエラーハンドラー
const errorHandler = async (c: any, next: any) => {
  try {
    await next()
  } catch (error) {
    console.error('Global error handler:', error)
    if (error instanceof AppError) {
      return c.json({
        error: error.code,
        message: error.message,
        details: error.details,
        timestamp: new Date().toISOString(),
        path: c.req.url
      }, error.statusCode)
    }
    // 予期しないエラー
    return c.json({
      error: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
      timestamp: new Date().toISOString(),
      path: c.req.url
    }, 500)
  }
}
app.use('*', errorHandler)
// 使用例
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  if (!id || !/^\d+$/.test(id)) {
    throw new AppError(400, 'INVALID_ID', 'User ID must be a positive integer')
  }
  const user = await getUserById(id)
  if (!user) {
    throw new AppError(404, 'USER_NOT_FOUND', `User with ID ${id} not found`)
  }
  return c.json(user)
})
「カスタムエラークラスを使うことで、エラーの詳細情報を構造化できますね。」
詳細なエラー分類
エラーコードの体系化
// エラーコード定義
export const ErrorCodes = {
  // バリデーションエラー
  VALIDATION_FAILED: 'VALIDATION_FAILED',
  INVALID_ID: 'INVALID_ID',
  INVALID_EMAIL: 'INVALID_EMAIL',
  REQUIRED_FIELD_MISSING: 'REQUIRED_FIELD_MISSING',
  // 認証・認可エラー
  AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
  INVALID_TOKEN: 'INVALID_TOKEN',
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
  INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
  // リソースエラー
  USER_NOT_FOUND: 'USER_NOT_FOUND',
  POST_NOT_FOUND: 'POST_NOT_FOUND',
  RESOURCE_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS',
  // ビジネスロジックエラー
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
  ORDER_ALREADY_PROCESSED: 'ORDER_ALREADY_PROCESSED',
  INVALID_OPERATION: 'INVALID_OPERATION',
  // システムエラー
  DATABASE_ERROR: 'DATABASE_ERROR',
  EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
  INTERNAL_ERROR: 'INTERNAL_ERROR'
} as const
type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]
// エラークラスの拡張
class ValidationError extends AppError {
  constructor(message: string, details?: any) {
    super(400, ErrorCodes.VALIDATION_FAILED, message, details)
  }
}
class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(404, `${resource.toUpperCase()}_NOT_FOUND`, `${resource} with ID ${id} not found`)
  }
}
class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(401, ErrorCodes.AUTHENTICATION_REQUIRED, message)
  }
}
class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(403, ErrorCodes.INSUFFICIENT_PERMISSIONS, message)
  }
}
非同期エラーのハンドリング
// 非同期処理をラップするヘルパー
const asyncHandler = (fn: Function) => {
  return async (c: any, next?: any) => {
    try {
      return await fn(c, next)
    } catch (error) {
      // エラーを上位のエラーハンドラーに渡す
      throw error
    }
  }
}
// 使用例
app.get('/users/:id', asyncHandler(async (c) => {
  const id = c.req.param('id')
  const user = await getUserById(id) // この処理でエラーが発生する可能性
  if (!user) {
    throw new NotFoundError('User', id)
  }
  return c.json(user)
}))
Zodバリデーションとエラーハンドリング
詳細なバリデーションエラー
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const CreateUserSchema = z.object({
  name: z.string()
    .min(1, 'Name is required')
    .max(100, 'Name must be less than 100 characters'),
  email: z.string()
    .email('Invalid email format'),
  age: z.number()
    .int('Age must be an integer')
    .min(0, 'Age must be positive')
    .max(150, 'Age must be realistic'),
  tags: z.array(z.string())
    .max(10, 'Maximum 10 tags allowed')
    .optional()
})
// カスタムバリデーションエラーハンドラー
const validationErrorHandler = (result: any, c: any) => {
  if (!result.success) {
    const errors = result.error.errors.map((err: any) => ({
      field: err.path.join('.'),
      message: err.message,
      code: err.code
    }))
    return c.json({
      error: ErrorCodes.VALIDATION_FAILED,
      message: 'Validation failed',
      details: errors,
      timestamp: new Date().toISOString(),
      path: c.req.url
    }, 400)
  }
}
app.post('/users',
  zValidator('json', CreateUserSchema, validationErrorHandler),
  asyncHandler(async (c) => {
    const userData = c.req.valid('json')
    const user = await createUser(userData)
    return c.json(user, 201)
  })
)
カスタムZodエラーメッセージ
const UserUpdateSchema = z.object({
  name: z.string()
    .min(1, { message: 'お名前は必須です' })
    .max(100, { message: 'お名前は100文字以内で入力してください' }),
  email: z.string()
    .email({ message: 'メールアドレスの形式が正しくありません' }),
  bio: z.string()
    .max(1000, { message: '自己紹介は1000文字以内で入力してください' })
    .optional()
}).refine(
  (data) => data.name !== 'admin',
  {
    message: 'この名前は使用できません',
    path: ['name']
  }
)
データベースエラーのハンドリング
Prismaエラーの処理
import { PrismaClientKnownRequestError, PrismaClientValidationError } from '@prisma/client'
const handlePrismaError = (error: any) => {
  if (error instanceof PrismaClientKnownRequestError) {
    switch (error.code) {
      case 'P2002': // Unique constraint violation
        return new AppError(409, ErrorCodes.RESOURCE_ALREADY_EXISTS,
          'A record with this information already exists')
      case 'P2025': // Record not found
        return new NotFoundError('Resource', 'unknown')
      case 'P2003': // Foreign key constraint violation
        return new ValidationError('Referenced resource does not exist')
      default:
        return new AppError(500, ErrorCodes.DATABASE_ERROR, 'Database operation failed')
    }
  }
  if (error instanceof PrismaClientValidationError) {
    return new ValidationError('Invalid data provided')
  }
  return new AppError(500, ErrorCodes.DATABASE_ERROR, 'Database error occurred')
}
app.post('/users', asyncHandler(async (c) => {
  try {
    const userData = await c.req.json()
    const user = await prisma.user.create({ data: userData })
    return c.json(user, 201)
  } catch (error) {
    throw handlePrismaError(error)
  }
}))
外部API連携のエラーハンドリング
HTTPクライアントエラーの処理
class ExternalServiceError extends AppError {
  constructor(service: string, statusCode: number, message: string) {
    super(502, ErrorCodes.EXTERNAL_SERVICE_ERROR,
      `External service ${service} error: ${message}`)
  }
}
const callExternalAPI = async (url: string, options: any) => {
  try {
    const response = await fetch(url, options)
    if (!response.ok) {
      throw new ExternalServiceError(
        'Payment Service',
        response.status,
        `HTTP ${response.status}: ${response.statusText}`
      )
    }
    return await response.json()
  } catch (error) {
    if (error instanceof ExternalServiceError) {
      throw error
    }
    // ネットワークエラーなど
    throw new AppError(503, ErrorCodes.EXTERNAL_SERVICE_ERROR,
      'External service is currently unavailable')
  }
}
リトライ機能付きエラーハンドリング
const withRetry = async <T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delay: number = 1000
): Promise<T> => {
  let lastError: any
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      // リトライしないエラーの判定
      if (error instanceof AppError && error.statusCode < 500) {
        throw error
      }
      if (attempt === maxRetries) {
        break
      }
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`)
      await new Promise(resolve => setTimeout(resolve, delay))
      delay *= 2 // Exponential backoff
    }
  }
  throw lastError
}
app.get('/external-data', asyncHandler(async (c) => {
  const data = await withRetry(() =>
    callExternalAPI('https://api.example.com/data', { method: 'GET' })
  )
  return c.json(data)
}))
ログとモニタリング
構造化ログ
interface LogContext {
  requestId: string
  userId?: string
  action: string
  resource?: string
  error?: {
    name: string
    message: string
    stack?: string
    code?: string
  }
  duration?: number
  statusCode: number
}
const logger = {
  error: (context: LogContext, message: string) => {
    console.error(JSON.stringify({
      level: 'error',
      message,
      timestamp: new Date().toISOString(),
      ...context
    }))
  },
  warn: (context: Partial<LogContext>, message: string) => {
    console.warn(JSON.stringify({
      level: 'warn',
      message,
      timestamp: new Date().toISOString(),
      ...context
    }))
  },
  info: (context: Partial<LogContext>, message: string) => {
    console.log(JSON.stringify({
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      ...context
    }))
  }
}
// 拡張されたエラーハンドラー
const enhancedErrorHandler = async (c: any, next: any) => {
  const requestId = crypto.randomUUID()
  const startTime = Date.now()
  c.set('requestId', requestId)
  try {
    await next()
    const duration = Date.now() - startTime
    logger.info({
      requestId,
      action: `${c.req.method} ${c.req.url}`,
      duration,
      statusCode: 200
    }, 'Request completed')
  } catch (error) {
    const duration = Date.now() - startTime
    const userId = c.get('user')?.id
    if (error instanceof AppError) {
      logger.warn({
        requestId,
        userId,
        action: `${c.req.method} ${c.req.url}`,
        duration,
        statusCode: error.statusCode,
        error: {
          name: error.name,
          message: error.message,
          code: error.code
        }
      }, 'Application error occurred')
      return c.json({
        error: error.code,
        message: error.message,
        details: error.details,
        timestamp: new Date().toISOString(),
        requestId
      }, error.statusCode)
    }
    // 予期しないエラー
    logger.error({
      requestId,
      userId,
      action: `${c.req.method} ${c.req.url}`,
      duration,
      statusCode: 500,
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      }
    }, 'Unexpected error occurred')
    return c.json({
      error: ErrorCodes.INTERNAL_ERROR,
      message: 'An unexpected error occurred',
      timestamp: new Date().toISOString(),
      requestId
    }, 500)
  }
}
app.use('*', enhancedErrorHandler)
エラー通知システム
Slack通知
const notifyError = async (error: AppError, context: LogContext) => {
  // 重要なエラーのみ通知
  if (error.statusCode >= 500) {
    const slackMessage = {
      text: `🚨 Server Error Alert`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Error:* ${error.code}\n*Message:* ${error.message}\n*Request:* ${context.action}\n*Request ID:* ${context.requestId}`
          }
        }
      ]
    }
    try {
      await fetch(process.env.SLACK_WEBHOOK_URL!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(slackMessage)
      })
    } catch (notificationError) {
      console.error('Failed to send Slack notification:', notificationError)
    }
  }
}
開発環境でのエラー表示
const isDevelopment = process.env.NODE_ENV === 'development'
const developmentErrorHandler = async (c: any, next: any) => {
  try {
    await next()
  } catch (error) {
    if (isDevelopment) {
      // 開発環境では詳細なエラー情報を返す
      return c.json({
        error: error.name,
        message: error.message,
        stack: error.stack,
        timestamp: new Date().toISOString()
      }, error.statusCode || 500)
    } else {
      // 本番環境では最小限の情報のみ
      return c.json({
        error: 'INTERNAL_ERROR',
        message: 'An error occurred',
        timestamp: new Date().toISOString()
      }, 500)
    }
  }
}
やってみよう!
堅牢なエラーハンドリングシステムを構築してみましょう:
- 
多層エラーハンドリング
- グローバルエラーハンドラー
 - 機能別エラーハンドラー
 - バリデーションエラーハンドラー
 
 - 
ログとモニタリング
- 構造化ログの実装
 - エラー通知システム
 - パフォーマンス監視
 
 - 
ユーザーフレンドリーなエラー
- 分かりやすいエラーメッセージ
 - 多言語対応
 - 回復可能なエラーの提示
 
 
ポイント
- 一貫したエラー形式:統一されたエラーレスポンス構造
 - 適切な分類:エラーコードによる体系的な分類
 - 詳細なログ:トラブルシューティングに必要な情報の記録
 - グレースフルな処理:予期しないエラーからのアプリケーション保護
 - 開発効率:開発環境での詳細なエラー情報提供