错误处理与故障排查
ZenWeb 提供了一套完整的错误处理机制,从业务逻辑 中的 fail() 调用到最终 HTTP 响应输出,整个过程由框架统一管理。本文档将详细介绍错误处理流程、消息代码系统、结果配置选项以及常见错误的排查方法。
错误处理流程总览
当请求进入 ZenWeb 应用后,错误处理遵循以下流程:
客户端请求
|
v
Koa 中间件 (failCatch)
|
v
路由匹配 → Controller 方法执行
|
|--- 正常返回 ----→ successWrap → ResultRender → HTTP 响应
|
|--- fail() 抛出 → ResultFail → failWrap → ResultRender → HTTP 响应
|
|--- 未捕获异常 → exposeUnexpected 判断 → ResultFail → HTTP 响应
核心流程说明:
- Koa 中间件层:
@zenweb/result模块注册的failCatch中间件在最外层捕获所有异常 - Controller 层:业务代码通过
fail()抛出ResultFail异常,或者发生未预期的Error - ResultFail 处理:中间件检测到
ResultFail实例后,设置 HTTP 状态码和X-Fail-Code头信息 - ResultRender 层:
RenderManager匹配合适的渲染器(默认JSONRender),将结果序列化为 JSON 输出
fail() 的各种调用方式
fail() 是 ZenWeb 中最常用的错误输出方法。它本质上是一个异常抛出函数,调用后代码不会继续执行,因此不需要 return。
import { Get, fail } from 'zenweb';
export class UserController {
// 方式一:仅传入错误消息字符串
@Get()
simpleError() {
fail('操作失败');
// 下面的代码不会执行
}
// 方式二:传入错误代码和消息
@Get()
codeAndMessage() {
fail(1001, '用户名已存在');
}
// 方式三:传入错误代码、消息和附加数据
@Get()
codeMessageAndData() {
fail(1002, '参数校验失败', { field: 'email', rule: 'required' });
}
// 方式四:传入完整的 ResultFailDetail 对象
@Get()
detailObject() {
fail({
code: 2001,
message: '余额不足',
data: { currentBalance: 50, required: 100 },
status: 400,
});
}
}
throw fail() 与直接调用 fail()
fail() 函数内部已经执行了 throw new ResultFail(...),所以直接调用 fail() 即可终止执行。但在某些场景下你可能需要 throw:
import { Get, fail } from 'zenweb';
export class OrderController {
@Get()
async create() {
// 直接调用 fail() — 代码立即终止,不需要 return
if (!this.validateOrder()) {
fail(3001, '订单数据无效');
}
// 在表达式中使用 throw fail()
// 这在三元表达式或箭头函数中非常有用
const user = await getUser() || throw fail('用户不存在');
// 在辅助函数中配合 throw 使用
this.checkPermission() || throw fail(4003, '无权限操作');
}
private validateOrder() {
return false;
}
private async getUser() {
return null;
}
private checkPermission() {
return false;
}
}
由于 fail() 的返回类型是 never,TypeScript 会正确推断出调用 fail() 之后的代码不可达, 因此不需要额外处理返回值。
ResultFail 对象结构
当 fail() 被调用时,框架会创建一个 ResultFail 实例。该类继承自 Error,包含以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
message | string | 错误消息文本 |
code | number | 业务错误代码 |
status | number | HTTP 状态码(覆盖默认值) |
data | unknown | 附加的错误数据 |
extra | Record<string, unknown> | 额外字段,会被展开到响应顶层 |
expose | boolean | 标识为可暴露信息,默认 true |
默认输出格式(未自定义 failWrap 时):
{
"code": 1001,
"message": "用户名已存在",
"data": null
}
使用 extra 字段时,额外数据会展开到响应顶层:
fail({
code: 4001,
message: '验证失败',
extra: {
errors: [
{ field: 'username', message: '用户名不能为空' },
{ field: 'email', message: '邮箱格式不正确' },
],
},
});
输出结果:
{
"code": 4001,
"message": "验证失败",
"errors": [
{ "field": "username", "message": "用户名不能为空" },
{ "field": "email", "message": "邮箱格式不正确" }
]
}
HTTP 状态码约定
ZenWeb 对错误场景的 HTTP 状态码有以下约定:
| 场景 | 默认状态码 | 说明 |
|---|---|---|
业务错误(fail()) | 422 | 通过 failStatus 配置,可被 ResultFail.status 覆盖 |
| 未预期异常 | 500 | 通过 unexpectedStatus 配置 |
| 路由未匹配 | 404 | Koa 默认行为 |
| 正常成功响应 | 200 | 控制器正常返回 |
import { Get, fail } from 'zenweb';
export class ApiController {
@Get()
businessError() {
// HTTP 422,默认业务错误状态码
fail('操作不允许');
}
@Get()
customStatus() {
// HTTP 403,自定义状态码
fail({
code: 4003,
message: '权限不足',
status: 403,
});
}
@Get()
notFound() {
// HTTP 404,资源不存在
fail({
code: 4004,
message: '用户不存在',
status: 404,
});
}
}
message-codes.json 详细使用
消息代码系统可以将错误提示与业务代码分离,统一管理所有提示信息。
文件格式
在项目根目录创建 message-codes.json 文件:
{
"user.notfound": "用户 {id} 不存在",
"user.disabled": "用户已被禁用,请联系管理员",
"user.username.short": "用户名长度不能少于 {min} 个字符",
"user.email.duplicate": "邮箱 {email} 已被注册",
"order.insufficient_balance": "余额不足,当前余额 {balance},需要 {required}",
"order.stock_zero": "商品 {name} 库存不足",
"auth.token.expired": "登录已过期,请重新登录",
"auth.permission.denied": "没有 {resource} 的 {action} 权限",
"400": "请求参数错误",
"404": "资源不存在"
}
参数替换
消息内容中使用 {key} 占位符,调用时传入参数对象进行替换:
import { Get, Context } from 'zenweb';
export class UserController {
@Get()
async info(ctx: Context) {
const userId = ctx.query.id;
const user = await findUser(userId);
if (!user) {
// 输出:用户 123 不存在
ctx.messageCodeResolver.fail('user.notfound', { id: userId });
}
return user;
}
@Get()
async register(ctx: Context) {
const username = ctx.query.username;
if (username.length < 3) {
// 输出:用户名长度不能少于 3 个字符
ctx.messageCodeResolver.fail('user.username.short', { min: 3 });
}
}
@Get()
async updateEmail(ctx: Context) {
const email = ctx.query.email;
const exists = await checkEmailExists(email);
if (exists) {
// 输出:邮箱 test@example.com 已被注册
ctx.messageCodeResolver.fail('user.email.duplicate', { email });
}
}
}
递归匹配机制
当消息代码为字符串类型时,系统会按照 . 分隔符逐级向上查找匹配项:
// 查找 "user.email.duplicate" 时的查找顺序:
// 1. "user.email.duplicate" → 匹配
// 如果不匹配则继续查找:
// 2. "user.email" → 未配置则继续
// 3. "user" → 未配置则继续
// 4. 都未匹配,直接使用代码字符串本身作为输出
这意味着你可以配置一个通用的前缀消息:
{
"user": "用户相关错误",
"user.notfound": "用户 {id} 不存在"
}
// 精确匹配到 "user.notfound"
ctx.messageCodeResolver.fail('user.notfound', { id: 123 });
// 输出:用户 123 不存在
// 未匹配到 "user.disabled",递归匹配到 "user"
ctx.messageCodeResolver.fail('user.disabled');
// 输出:用户相关错误
format() 方法
如果只需要获取格式化后的消息而不抛出异常,可以使用 format() 方法:
import { Get, Context } from 'zenweb';
export class LogController {
@Get()
async test(ctx: Context) {
// 获取格式化后的消息字符串
const msg = ctx.messageCodeResolver.format('user.notfound', { id: 123 });
console.log(msg); // "用户 123 不存在"
// 检查消息代码是否存在
const exists = ctx.messageCodeResolver.has('user.notfound');
console.log(exists); // true
return { message: msg };
}
}
加载多个消息代码文件
通过 autoLoadFilenames 配置项可以加载多个消息代码文件,支持 .json 和 .js 格式:
import { Core } from '@zenweb/core';
import inject from '@zenweb/inject';
import result from '@zenweb/result';
import messagecode from '@zenweb/messagecode';
const app = new Core();
app.setup(inject);
app.setup(result);
app.setup(messagecode, {
// 同时加载多个消息代码文件
autoLoadFilenames: [
'./message-codes.json', // 通用消息
'./message-codes-order.json', // 订单模块消息
'./message-codes-user.json', // 用户模块消息
],
// 也可以直接在代码中定义
codes: {
'custom.error': '自定义错误消息',
},
});
.js 文件需要使用 export default 导出代码映射对象,适合需要动态生成消息的场景。
result 配置选项
@zenweb/result 模块支持丰富的配置选项,用于控制错误输出行为。
基础配置
import { Core } from '@zenweb/core';
import inject from '@zenweb/inject';
import result from '@zenweb/result';
const app = new Core();
app.setup(inject);
app.setup(result, {
// 默认业务错误代码(当 fail() 未指定 code 时使用)
failCode: -1,
// 默认业务错误 HTTP 状态码
failStatus: 422,
// 默认错误消息
failMessage: '请求处理失败',
// 是否在响应头中输出错误代码
// true → 使用默认头名称 "X-Fail-Code"
// 字符串 → 自定义头名称
// false → 不输出
failCodeHeader: true,
// 是否暴露未预期异常的详细信息
// 生产环境建议设为 false
exposeUnexpected: process.env.NODE_ENV !== 'production',
// 未预期异常的 HTTP 状态码
unexpectedStatus: 500,
// 未预期异常的错误代码
unexpectedCode: 500,
});
failCodeHeader 响应头
当启用 failCodeHeader 后,业务错误会在响应头中包含错误代码,方便前端或网关层快速判断错误类型:
// 配置
failCodeHeader: true
// 响应头
// X-Fail-Code: 1001
// 自定义头名称
failCodeHeader: 'X-Error-Code'
// 响应头
// X-Error-Code: 1001
exposeUnexpected 暴露异常详情
当控制器中抛出未预期的 Error(非 ResultFail)时:
// 开发环境 exposeUnexpected: true
// 响应内容包含错误堆栈:
{
"code": 500,
"message": "Cannot read properties of undefined (reading 'name')",
"data": {
"name": "TypeError",
"stack": ["at UserController.info (/src/controller.ts:12:8)"]
}
}
// 生产环境 exposeUnexpected: false
// 异常不会被 @zenweb/result 处理,而是继续向外抛出
// 由 Koa 或外层中间件处理