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
关键术语:AppError、handleError、错误码、日志记录
示例 Prompt:
请创建一个统一的错误处理系统:
1. 自定义错误类:NotFound、Unauthorized、Forbidden、Conflict
2. 错误处理函数:识别不同错误类型,返回对应状态码
3. 处理 Prisma 错误:P2002 唯一约束、P2025 记录不存在
4. 开发环境显示详细错误,生产环境隐藏细节避坑指南
- 不要暴露敏感信息:生产环境不要返回堆栈信息
- 使用语义化状态码:不要所有错误都返回 500
- 记录足够的上下文:便于排查问题
- 前后端错误码统一:定义好文档
验收清单
- [ ] 有自定义错误类体系
- [ ] 有统一的错误处理函数
- [ ] 错误响应格式一致
- [ ] 有适当的日志记录
