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

9.4.4 崩了也要优雅——错误恢复:异常处理与用户提示

用户不需要知道数据库连接超时——他们只需要知道"请稍后重试"。

错误处理层次

自定义错误类

typescript
// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = 'AppError';
  }
}

export class ValidationError extends AppError {
  constructor(message: string, public field?: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource}不存在`, 'NOT_FOUND', 404);
  }
}

export class AuthenticationError extends AppError {
  constructor(message = '请先登录') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

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

export class ConflictError extends AppError {
  constructor(message: string) {
    super(message, 'CONFLICT', 409);
  }
}

export class RateLimitError extends AppError {
  constructor() {
    super('请求太频繁,请稍后再试', 'RATE_LIMIT', 429);
  }
}

全局错误处理

typescript
// lib/error-handler.ts
import { NextResponse } from 'next/server';
import { logger } from './logger';
import { AppError } from './errors';

export function handleError(err: unknown) {
  // 已知业务错误
  if (err instanceof AppError) {
    logger.warn({
      code: err.code,
      message: err.message,
      statusCode: err.statusCode,
    }, '业务错误');
    
    return NextResponse.json(
      { error: { code: err.code, message: err.message } },
      { status: err.statusCode }
    );
  }
  
  // Prisma 错误
  if (err instanceof Prisma.PrismaClientKnownRequestError) {
    return handlePrismaError(err);
  }
  
  // 未知错误
  logger.error({ err }, '未处理的错误');
  
  return NextResponse.json(
    { error: { code: 'INTERNAL_ERROR', message: '服务暂时不可用,请稍后重试' } },
    { status: 500 }
  );
}

function handlePrismaError(err: Prisma.PrismaClientKnownRequestError) {
  switch (err.code) {
    case 'P2002':
      return NextResponse.json(
        { error: { code: 'DUPLICATE', message: '数据已存在' } },
        { status: 409 }
      );
    case 'P2025':
      return NextResponse.json(
        { error: { code: 'NOT_FOUND', message: '数据不存在' } },
        { status: 404 }
      );
    default:
      logger.error({ err, code: err.code }, 'Prisma 错误');
      return NextResponse.json(
        { error: { code: 'DATABASE_ERROR', message: '数据库操作失败' } },
        { status: 500 }
      );
  }
}

API 路由使用

typescript
// app/api/orders/route.ts
import { handleError } from '@/lib/error-handler';
import { ValidationError, NotFoundError } from '@/lib/errors';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    // 业务验证
    if (!body.items?.length) {
      throw new ValidationError('请选择商品', 'items');
    }
    
    // 检查库存
    const outOfStock = await checkStock(body.items);
    if (outOfStock.length) {
      throw new ValidationError(
        `以下商品库存不足:${outOfStock.join('、')}`
      );
    }
    
    const order = await orderService.create(body);
    return NextResponse.json(order, { status: 201 });
    
  } catch (err) {
    return handleError(err);
  }
}

重试机制

typescript
// lib/retry.ts
interface RetryOptions {
  maxRetries?: number;
  delay?: number;
  backoff?: number;
  shouldRetry?: (err: Error) => boolean;
}

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const {
    maxRetries = 3,
    delay = 1000,
    backoff = 2,
    shouldRetry = () => true,
  } = options;
  
  let lastError: Error;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err as Error;
      
      if (attempt === maxRetries || !shouldRetry(lastError)) {
        throw lastError;
      }
      
      logger.warn({
        attempt: attempt + 1,
        maxRetries,
        error: lastError.message,
      }, '操作失败,准备重试');
      
      await sleep(delay * Math.pow(backoff, attempt));
    }
  }
  
  throw lastError!;
}
typescript
// 使用重试
const result = await withRetry(
  () => externalApi.call(data),
  {
    maxRetries: 3,
    delay: 1000,
    shouldRetry: (err) => err.message.includes('timeout'),
  }
);

降级处理

typescript
// services/recommendation.service.ts
export async function getRecommendations(userId: string) {
  try {
    return await recommendationApi.get(userId);
  } catch (err) {
    logger.warn({ err, userId }, '推荐服务不可用,使用降级数据');
    
    // 降级到热门商品
    return await getPopularProducts();
  }
}

用户友好的错误消息

typescript
// lib/user-messages.ts
const errorMessages: Record<string, string> = {
  VALIDATION_ERROR: '请检查输入信息',
  NOT_FOUND: '找不到相关内容',
  UNAUTHORIZED: '请先登录',
  FORBIDDEN: '没有权限',
  RATE_LIMIT: '操作太频繁,请稍后再试',
  PAYMENT_FAILED: '支付失败,请重试或更换支付方式',
  STOCK_INSUFFICIENT: '库存不足',
  INTERNAL_ERROR: '服务暂时不可用,请稍后重试',
};

export function getUserMessage(code: string): string {
  return errorMessages[code] || '发生未知错误,请稍后重试';
}

前端错误处理

typescript
// lib/api-client.ts
export async function apiRequest<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, options);
  
  if (!response.ok) {
    const error = await response.json();
    
    // 显示用户友好的错误消息
    toast.error(error.error?.message || '请求失败');
    
    throw new ApiError(
      error.error?.code || 'UNKNOWN',
      error.error?.message || '请求失败',
      response.status
    );
  }
  
  return response.json();
}

本节小结

错误恢复的核心是对外友好、对内详细。用户看到的是简洁的错误提示,开发者看到的是完整的错误上下文。通过自定义错误类区分业务错误和系统错误,通过重试和降级提高系统韧性。