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();
}本节小结
错误恢复的核心是对外友好、对内详细。用户看到的是简洁的错误提示,开发者看到的是完整的错误上下文。通过自定义错误类区分业务错误和系统错误,通过重试和降级提高系统韧性。
