Koa 错误处理实现:全局兜底 + 精细化区分
以下是完整的 Koa 错误处理方案,涵盖自定义错误类型、全局错误中间件、结构化日志、异步错误兜底,所有代码可直接运行,且严格遵循「业务错误暴露详情、系统错误隐藏详情」的核心要求。
一、环境准备
先安装依赖:
1 2
| npm install koa koa-router koa-body winston http-errors
|
二、目录结构
1 2 3 4 5 6 7 8 9 10
| src/ ├── errors/ # 自定义错误类 │ └── customError.js ├── logger/ # 结构化日志配置 │ └── index.js ├── middleware/ # 全局错误中间件 │ └── errorHandler.js ├── routes/ # 示例路由(模拟错误场景) │ └── demo.js └── app.js # 入口文件
|
三、核心代码实现
1. 自定义错误类(区分业务/系统错误)
src/errors/customError.js:定义标准化的错误基类,明确区分「业务错误」和「系统错误」,便于后续精细化处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
class CustomError extends Error { constructor(code, message) { super(message); this.code = code; this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } }
class BusinessError extends CustomError { constructor(code, message) { super(code, message); } }
class SystemError extends CustomError { constructor(code = 500, message) { super(code, message); } }
module.exports = { CustomError, BusinessError, SystemError };
|
2. 结构化日志配置(winston)
src/logger/index.js:记录错误的完整上下文(请求参数、IP、堆栈、时间),输出到文件/控制台,便于问题溯源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| const winston = require('winston');
const { combine, timestamp, printf, json } = winston.format;
const logFormat = printf(({ level, message, timestamp, ...meta }) => { return JSON.stringify({ time: timestamp, level: level, message: message, ...meta }); });
const logger = winston.createLogger({ level: 'error', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), json(), logFormat ), transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), logFormat ) }) ] });
module.exports = logger;
|
3. 全局错误中间件(核心)
src/middleware/errorHandler.js:捕获所有同步/异步错误,统一返回格式,区分业务/系统错误处理逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| const logger = require('../logger'); const { BusinessError, SystemError } = require('../errors/customError');
const errorHandler = async (ctx, next) => { try { await next(); } catch (err) { const errorContext = { request: { method: ctx.method, url: ctx.url, params: ctx.params, query: ctx.query, body: ctx.request.body, ip: ctx.ip }, stack: err.stack, errorName: err.name, errorMessage: err.message };
logger.error('Request Error', errorContext);
const response = { code: 500, msg: '服务暂不可用', data: null };
if (err instanceof BusinessError) { response.code = err.code; response.msg = err.message; } else if (err instanceof SystemError) { response.code = err.code; } else { logger.error('Unknown Error', { ...errorContext, rawError: err }); }
ctx.status = err.code || 500; ctx.body = response; } };
module.exports = errorHandler;
|
4. 示例路由(模拟错误场景)
src/routes/demo.js:模拟不同错误场景(参数错误、权限不足、数据库异常),验证错误处理逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const Router = require('koa-router'); const router = new Router(); const { BusinessError, SystemError } = require('../errors/customError');
router.get('/user/:id', async (ctx) => { const { id } = ctx.params; if (!/^\d+$/.test(id)) { throw new BusinessError(400, '用户ID必须为数字'); } ctx.body = { code: 200, msg: 'success', data: { id } }; });
router.post('/admin/delete', async (ctx) => { const { role } = ctx.request.body; if (role !== 'admin') { throw new BusinessError(403, '权限不足,仅管理员可操作'); } ctx.body = { code: 200, msg: '删除成功', data: null }; });
router.get('/data/list', async (ctx) => { try { throw new Error('数据库连接超时(原生错误)'); } catch (dbErr) { throw new SystemError(500, dbErr.message); } });
module.exports = router;
|
5. 入口文件(整合所有模块 + 异步错误兜底)
src/app.js:注册中间件/路由,配置 Koa 内置的 error 事件兜底(防止未捕获的异步错误导致进程崩溃)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const Koa = require('koa'); const app = new Koa(); const koaBody = require('koa-body'); const router = require('./routes/demo'); const errorHandler = require('./middleware/errorHandler'); const logger = require('./logger');
app.use(koaBody());
app.use(errorHandler);
app.use(router.routes()).use(router.allowedMethods());
app.on('error', (err, ctx) => { const errorContext = { request: ctx ? { method: ctx.method, url: ctx.url, ip: ctx.ip } : '无请求上下文', stack: err.stack, errorMessage: err.message }; logger.error('Uncaught Koa Error (终极兜底)', errorContext); });
const PORT = 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });
|
四、测试验证
启动服务:node src/app.js,访问以下接口验证效果:
| 接口 |
响应结果(客户端) |
日志记录(服务端) |
| GET /user/abc |
{code:400,msg:”用户ID必须为数字”,data:null} |
包含请求参数、IP、堆栈、错误描述 |
| POST /admin/delete(body {role:”user”}) |
{code:403,msg:”权限不足,仅管理员可操作”,data:null} |
完整上下文 + 业务错误详情 |
| GET /data/list |
{code:500,msg:”服务暂不可用”,data:null} |
包含“数据库连接超时”原始错误 + 堆栈 |
五、关键注意事项
- 中间件顺序:错误中间件必须放在所有路由/业务中间件之前(洋葱模型特性,确保包裹所有逻辑);
- 异步错误捕获:Koa 中
async/await 的错误会被 try/catch 捕获,app.on('error') 处理「未被中间件捕获的错误」(如中间件外的异步操作);
- 日志溯源:堆栈信息(
err.stack)是定位问题的核心,必须记录;
- 生产环境优化:日志可输出到 ELK 平台(Logstash 收集 + Kibana 可视化),而非仅本地文件。