⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.6.4 统一处理报错——错误处理

一句话破题

用自定义错误类和统一处理器,让 API 的错误响应既对用户友好,又便于调试。

核心价值

分散的 try-catch 让代码臃肿,不一致的错误格式让前端抓狂。统一的错误处理让代码更简洁,让错误信息更规范。

自定义错误类

tsx
// lib/errors.ts

export class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code: string = 'INTERNAL_ERROR'
  ) {
    super(message)
    this.name = this.constructor.name
  }
}

export class NotFoundError extends AppError {
  constructor(message = '资源不存在') {
    super(message, 404, 'NOT_FOUND')
  }
}

export class ValidationError extends AppError {
  constructor(message = '参数验证失败', public details?: unknown) {
    super(message, 400, 'VALIDATION_ERROR')
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = '未登录或登录已过期') {
    super(message, 401, 'UNAUTHORIZED')
  }
}

export class ForbiddenError extends AppError {
  constructor(message = '无权限执行此操作') {
    super(message, 403, 'FORBIDDEN')
  }
}

export class ConflictError extends AppError {
  constructor(message = '资源冲突') {
    super(message, 409, 'CONFLICT')
  }
}

统一错误处理器

tsx
// lib/errorHandler.ts

import { AppError } from './errors'
import { Prisma } from '@prisma/client'

interface ErrorResponse {
  error: {
    code: string
    message: string
    details?: unknown
  }
}

export function handleError(error: unknown): Response {
  console.error('API Error:', error)
  
  if (error instanceof AppError) {
    return createErrorResponse(error.statusCode, {
      code: error.code,
      message: error.message,
      details: (error as any).details,
    })
  }
  
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    return handlePrismaError(error)
  }
  
  if (error instanceof Prisma.PrismaClientValidationError) {
    return createErrorResponse(400, {
      code: 'VALIDATION_ERROR',
      message: '数据格式错误',
    })
  }
  
  return createErrorResponse(500, {
    code: 'INTERNAL_ERROR',
    message: process.env.NODE_ENV === 'development' 
      ? String(error) 
      : '服务器内部错误',
  })
}

function handlePrismaError(error: Prisma.PrismaClientKnownRequestError): Response {
  switch (error.code) {
    case 'P2002':
      const field = (error.meta?.target as string[])?.[0] || '字段'
      return createErrorResponse(409, {
        code: 'CONFLICT',
        message: `${field} 已存在`,
      })
    case 'P2025':
      return createErrorResponse(404, {
        code: 'NOT_FOUND',
        message: '记录不存在',
      })
    default:
      return createErrorResponse(500, {
        code: 'DATABASE_ERROR',
        message: '数据库操作失败',
      })
  }
}

function createErrorResponse(
  status: number, 
  error: ErrorResponse['error']
): Response {
  return Response.json({ error }, { status })
}

在 API Route 中使用

tsx
// app/api/posts/[id]/route.ts
import { postService } from '@/services/postService'
import { handleError } from '@/lib/errorHandler'
import { updatePostSchema } from '@/lib/validations/post'

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  try {
    const post = await postService.findById(params.id)
    return Response.json({ data: post })
  } catch (error) {
    return handleError(error)
  }
}

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json()
    const result = updatePostSchema.safeParse(body)
    
    if (!result.success) {
      return Response.json(
        { error: { code: 'VALIDATION_ERROR', details: result.error.flatten() } },
        { status: 400 }
      )
    }
    
    const post = await postService.update(params.id, result.data)
    return Response.json({ data: post })
  } catch (error) {
    return handleError(error)
  }
}

封装高阶函数

进一步简化 Route Handler:

tsx
// lib/apiHandler.ts
import { handleError } from './errorHandler'

type ApiHandler = (
  request: Request,
  context?: { params: Record<string, string> }
) => Promise<Response>

export function withErrorHandler(handler: ApiHandler): ApiHandler {
  return async (request, context) => {
    try {
      return await handler(request, context)
    } catch (error) {
      return handleError(error)
    }
  }
}

// 使用
export const GET = withErrorHandler(async (request, { params }) => {
  const post = await postService.findById(params.id)
  return Response.json({ data: post })
})

错误响应格式规范

tsx
// 统一的错误响应格式
{
  "error": {
    "code": "NOT_FOUND",           // 机器可读的错误码
    "message": "文章不存在",         // 用户可读的错误信息
    "details": {                    // 可选,详细信息
      "fieldErrors": {
        "title": ["标题不能为空"]
      }
    }
  }
}

// 常用错误码
// VALIDATION_ERROR - 参数验证失败
// NOT_FOUND - 资源不存在
// UNAUTHORIZED - 未认证
// FORBIDDEN - 无权限
// CONFLICT - 资源冲突
// INTERNAL_ERROR - 服务器错误

日志记录

tsx
// lib/logger.ts
export function logError(error: unknown, context?: Record<string, unknown>) {
  const timestamp = new Date().toISOString()
  const errorInfo = {
    timestamp,
    error: error instanceof Error ? {
      name: error.name,
      message: error.message,
      stack: error.stack,
    } : String(error),
    context,
  }
  
  console.error(JSON.stringify(errorInfo))
  
  if (process.env.NODE_ENV === 'production') {
    // 发送到日志服务
  }
}

// 在 errorHandler 中使用
export function handleError(error: unknown, context?: Record<string, unknown>): Response {
  logError(error, context)
  // ...
}

前端错误处理

配合前端统一处理 API 错误:

tsx
// 前端 API 调用封装
async function apiRequest<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  })
  
  const data = await response.json()
  
  if (!response.ok) {
    throw new ApiError(data.error.code, data.error.message, response.status)
  }
  
  return data.data
}

// 使用
try {
  const post = await apiRequest('/api/posts/123')
} catch (error) {
  if (error instanceof ApiError) {
    if (error.code === 'NOT_FOUND') {
      // 显示 404 页面
    } else {
      // 显示错误提示
      toast.error(error.message)
    }
  }
}

AI 协作指南

核心意图:让 AI 帮你设计统一的错误处理机制。

需求定义公式

  • 功能描述:创建统一的错误处理系统
  • 错误类型:[列出需要处理的错误类型]
  • 响应格式:code + message + details

关键术语AppErrorhandleError、错误码、日志记录

示例 Prompt

请创建一个统一的错误处理系统:
1. 自定义错误类:NotFound、Unauthorized、Forbidden、Conflict
2. 错误处理函数:识别不同错误类型,返回对应状态码
3. 处理 Prisma 错误:P2002 唯一约束、P2025 记录不存在
4. 开发环境显示详细错误,生产环境隐藏细节

避坑指南

  1. 不要暴露敏感信息:生产环境不要返回堆栈信息
  2. 使用语义化状态码:不要所有错误都返回 500
  3. 记录足够的上下文:便于排查问题
  4. 前后端错误码统一:定义好文档

验收清单

  • [ ] 有自定义错误类体系
  • [ ] 有统一的错误处理函数
  • [ ] 错误响应格式一致
  • [ ] 有适当的日志记录