D8D Starter 错误处理规范
版本信息
| 版本 |
日期 |
描述 |
作者 |
| 1.0 |
2025-09-15 |
初始错误处理规范 |
Sarah (PO) |
1. 错误处理原则
1.1 核心原则
- 一致性: 所有错误响应格式统一
- 安全性: 不泄露敏感信息到客户端
- 可操作性: 错误信息应指导用户或开发者
- 可追溯性: 错误应包含足够上下文用于调试
1.2 错误分类
按严重程度
- 🔴 致命错误: 系统无法继续运行
- 🟡 业务错误: 用户操作失败,可恢复
- 🔵 客户端错误: 用户输入或配置问题
- ⚪ 信息性错误: 警告或提示信息
按来源
- 验证错误: 输入数据验证失败
- 业务逻辑错误: 业务规则违反
- 系统错误: 基础设施或第三方服务故障
- 网络错误: 连接超时或中断
2. 错误响应格式
2.1 标准错误响应
// 成功响应
{
"success": true,
"data": { /* 业务数据 */ },
"message": "操作成功"
}
// 错误响应
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "输入数据验证失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
}
],
"timestamp": "2025-09-15T10:30:00Z",
"requestId": "req_1234567890"
}
}
2.2 HTTP状态码映射
| 错误类型 |
HTTP状态码 |
错误代码 |
描述 |
| 验证错误 |
400 |
VALIDATION_ERROR |
输入数据验证失败 |
| 未授权 |
401 |
UNAUTHORIZED |
需要认证 |
| 权限不足 |
403 |
FORBIDDEN |
权限不足 |
| 资源不存在 |
404 |
NOT_FOUND |
资源未找到 |
| 业务错误 |
409 |
BUSINESS_ERROR |
业务规则冲突 |
| 系统错误 |
500 |
INTERNAL_ERROR |
服务器内部错误 |
| 服务不可用 |
503 |
SERVICE_UNAVAILABLE |
服务暂时不可用 |
3. 错误代码规范
3.1 错误代码命名约定
- 使用
SCREAMING_SNAKE_CASE
- 前缀表示错误类别
- 后缀表示具体错误
3.2 标准错误代码
认证错误 (AUTH_*)
export const AUTH_ERRORS = {
UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
PERMISSION_DENIED: 'AUTH_PERMISSION_DENIED'
} as const;
验证错误 (VALIDATION_*)
export const VALIDATION_ERRORS = {
REQUIRED_FIELD: 'VALIDATION_REQUIRED_FIELD',
INVALID_EMAIL: 'VALIDATION_INVALID_EMAIL',
INVALID_PASSWORD: 'VALIDATION_INVALID_PASSWORD',
UNIQUE_CONSTRAINT: 'VALIDATION_UNIQUE_CONSTRAINT',
OUT_OF_RANGE: 'VALIDATION_OUT_OF_RANGE'
} as const;
业务错误 (BUSINESS_*)
export const BUSINESS_ERRORS = {
USER_NOT_FOUND: 'BUSINESS_USER_NOT_FOUND',
INSUFFICIENT_BALANCE: 'BUSINESS_INSUFFICIENT_BALANCE',
OPERATION_NOT_ALLOWED: 'BUSINESS_OPERATION_NOT_ALLOWED',
RESOURCE_CONFLICT: 'BUSINESS_RESOURCE_CONFLICT'
} as const;
系统错误 (SYSTEM_*)
export const SYSTEM_ERRORS = {
DATABASE_ERROR: 'SYSTEM_DATABASE_ERROR',
NETWORK_ERROR: 'SYSTEM_NETWORK_ERROR',
EXTERNAL_SERVICE_ERROR: 'SYSTEM_EXTERNAL_SERVICE_ERROR',
CONFIGURATION_ERROR: 'SYSTEM_CONFIGURATION_ERROR'
} as const;
4. 实现指南
4.1 后端错误处理
错误类定义
// src/shared/errors/AppError.ts
export class AppError extends Error {
public readonly code: string;
public readonly statusCode: number;
public readonly details?: any;
public readonly timestamp: Date;
constructor(
code: string,
message: string,
statusCode: number = 500,
details?: any
) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.details = details;
this.timestamp = new Date();
// 保持正确的堆栈跟踪
Error.captureStackTrace(this, this.constructor);
}
toJSON() {
return {
code: this.code,
message: this.message,
details: this.details,
timestamp: this.timestamp.toISOString()
};
}
}
// 特定错误类
export class ValidationError extends AppError {
constructor(message: string, details?: any) {
super('VALIDATION_ERROR', message, 400, details);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id?: string | number) {
const message = id
? `${resource} with ID ${id} not found`
: `${resource} not found`;
super('NOT_FOUND', message, 404);
}
}
错误处理中间件
// src/server/middleware/errorHandler.ts
import { Context } from 'hono';
import { AppError } from '../../shared/errors/AppError';
export async function errorHandler(err: Error, c: Context) {
// 记录错误
console.error('Error:', {
message: err.message,
stack: err.stack,
url: c.req.url,
method: c.req.method,
timestamp: new Date().toISOString()
});
// AppError 实例
if (err instanceof AppError) {
return c.json(
{
success: false,
error: {
code: err.code,
message: err.message,
details: err.details,
timestamp: err.timestamp.toISOString(),
requestId: c.get('requestId')
}
},
err.statusCode
);
}
// 未知错误(生产环境隐藏细节)
const isProduction = process.env.NODE_ENV === 'production';
return c.json(
{
success: false,
error: {
code: 'INTERNAL_ERROR',
message: isProduction ? 'Internal server error' : err.message,
timestamp: new Date().toISOString(),
requestId: c.get('requestId')
}
},
500
);
}
使用示例
// 在服务中使用
export class UserService {
async getUserById(id: number) {
const user = await User.findOne({ where: { id } });
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
async createUser(data: CreateUserDto) {
// 验证输入
const errors = validateUserData(data);
if (errors.length > 0) {
throw new ValidationError('Invalid user data', errors);
}
// 检查唯一性
const existingUser = await User.findOne({
where: [{ email: data.email }, { username: data.username }]
});
if (existingUser) {
throw new BusinessError('USER_ALREADY_EXISTS', 'User with this email or username already exists', 409);
}
// 创建用户
try {
return await User.create(data).save();
} catch (error) {
throw new DatabaseError('Failed to create user', error);
}
}
}
4.2 前端错误处理
错误处理钩子
// src/client/hooks/useErrorHandler.ts
import { toast } from 'sonner';
export function useErrorHandler() {
const handleError = (error: unknown) => {
console.error('Application error:', error);
if (error instanceof Error) {
// 显示用户友好的错误消息
toast.error(getUserFriendlyMessage(error));
} else {
toast.error('发生未知错误,请重试');
}
};
const getUserFriendlyMessage = (error: Error): string => {
const message = error.message.toLowerCase();
if (message.includes('network')) {
return '网络连接失败,请检查网络设置';
}
if (message.includes('validation')) {
return '输入数据验证失败,请检查表单';
}
if (message.includes('permission') || message.includes('auth')) {
return '权限不足,请重新登录';
}
return error.message || '操作失败,请重试';
};
return { handleError, getUserFriendlyMessage };
}
API客户端错误处理
// src/client/api.ts
import { toast } from 'sonner';
const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
try {
const response = await axios.request({
url: url.toString(),
method: init?.method || 'GET',
headers: init?.headers,
data: init?.body,
});
// 处理错误响应
if (response.status >= 400) {
const errorData = response.data?.error || {};
throw new ApiError(
errorData.message || '请求失败',
response.status,
errorData.code,
errorData.details
);
}
return createResponse(response);
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data?.error || {};
throw new ApiError(
errorData.message || error.message,
error.response?.status || 500,
errorData.code,
errorData.details
);
}
throw error;
}
};
class ApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string,
public details?: any
) {
super(message);
this.name = 'ApiError';
}
}
5. 日志和监控
5.1 错误日志格式
// 错误日志示例
{
"level": "error",
"timestamp": "2025-09-15T10:30:00.000Z",
"message": "User creation failed",
"code": "VALIDATION_ERROR",
"stack": "Error: Invalid email format...",
"context": {
"userId": 123,
"email": "invalid-email",
"requestId": "req_1234567890",
"url": "/api/v1/users",
"method": "POST"
}
}
5.2 监控配置
// 错误监控集成
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
// 在错误处理中间件中
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(err, {
extra: {
url: c.req.url,
method: c.req.method,
requestId: c.get('requestId')
}
});
}
6. 测试错误场景
6.1 错误测试示例
// 测试验证错误
describe('UserService - Error Handling', () => {
it('should throw ValidationError for invalid email', async () => {
const invalidData = { email: 'invalid', password: '123' };
await expect(userService.createUser(invalidData))
.rejects
.toThrow(ValidationError);
await expect(userService.createUser(invalidData))
.rejects
.toMatchObject({
code: 'VALIDATION_ERROR',
statusCode: 400
});
});
it('should throw NotFoundError for non-existent user', async () => {
await expect(userService.getUserById(999))
.rejects
.toThrow(NotFoundError);
});
});
// 测试错误响应格式
describe('Error Handler Middleware', () => {
it('should return formatted error response', async () => {
const server = createTestServer();
const response = await server.inject({
method: 'GET',
url: '/api/v1/users/999'
});
expect(response.statusCode).toBe(404);
expect(response.json()).toEqual({
success: false,
error: {
code: 'NOT_FOUND',
message: 'User with ID 999 not found',
timestamp: expect.any(String),
requestId: expect.any(String)
}
});
});
});
7. 最佳实践清单
7.1 错误处理检查清单
7.2 安全注意事项
- ❌ 不要返回堆栈跟踪到客户端
- ❌ 不要泄露数据库错误细节
- ❌ 不要暴露敏感配置信息
- ✅ 使用通用的错误消息
- ✅ 记录详细错误到服务器日志
- ✅ 使用请求ID关联错误
最后更新: 2025-09-15
维护者: 开发团队
文档状态: 正式版