跳到主要内容

Koa错误处理机制

1. 错误处理基础

在任何Web应用中,错误处理都是一个至关重要的环节。Koa作为一个现代化的Node.js框架,提供了优雅且强大的错误处理机制。本章将深入探讨Koa中的错误处理方法、最佳实践和高级技巧。

1.1 错误的类型

在Koa应用中,可能会遇到以下几种类型的错误:

  • 同步错误:在同步代码中抛出的错误
  • 异步错误:在异步操作中产生的错误
  • HTTP错误:与HTTP请求/响应相关的错误(如404、401、500等)
  • 业务逻辑错误:应用业务逻辑产生的错误
  • 系统错误:底层系统或基础设施产生的错误

1.2 Koa错误处理的特点

  • 基于Promise:Koa的错误处理基于Promise,使其能够优雅地处理异步错误
  • 洋葱模型:错误可以在中间件链中传递
  • 错误事件:Koa应用实例可以触发error事件
  • 错误级联:错误会沿着中间件链向上传播

2. 基本错误处理

2.1 try/catch处理同步错误

对于同步代码,可以使用try/catch捕获错误:

app.use(async (ctx, next) => {
try {
// 可能产生错误的同步代码
const result = syncOperation();
ctx.body = result;
} catch (error) {
ctx.status = 500;
ctx.body = { message: error.message };
}
});

2.2 await/try/catch处理异步错误

对于异步代码,结合await和try/catch可以捕获错误:

app.use(async (ctx, next) => {
try {
// 可能产生错误的异步代码
const result = await asyncOperation();
ctx.body = result;
} catch (error) {
ctx.status = 500;
ctx.body = { message: error.message };
}
});

2.3 使用ctx.throw()抛出错误

Koa提供了ctx.throw()方法来抛出HTTP错误:

app.use(async ctx => {
// 抛出400错误
if (!ctx.query.id) {
ctx.throw(400, 'Missing ID parameter');
}

// 抛出404错误
const user = await getUser(ctx.query.id);
if (!user) {
ctx.throw(404, 'User not found');
}

ctx.body = user;
});

3. 错误处理中间件

3.1 全局错误处理中间件

在Koa中,通常会创建一个全局错误处理中间件,放在所有中间件的最前面:

app.use(async (ctx, next) => {
try {
await next(); // 传递请求给下一个中间件

// 处理404错误
if (ctx.status === 404 && !ctx.body) {
ctx.status = 404;
ctx.body = {
code: 404,
message: 'Not Found'
};
}
} catch (error) {
// 设置错误状态码,默认为500
ctx.status = error.status || 500;

// 设置错误响应体
ctx.body = {
code: error.code || ctx.status,
message: error.message || 'Internal Server Error'
};

// 触发app.onerror事件
ctx.app.emit('error', error, ctx);
}
});

3.2 错误事件监听

可以监听Koa应用实例的error事件来记录错误日志或执行其他错误处理逻辑:

// 监听error事件
app.on('error', (error, ctx) => {
// 记录错误日志
console.error('Server error:', error);

// 可以在这里添加更多错误处理逻辑
// 例如:发送错误通知、记录到监控系统等

// 注意:如果没有ctx对象,说明错误发生在请求处理之外
if (ctx) {
console.error(`Request URL: ${ctx.url}`);
console.error(`Request Method: ${ctx.method}`);
console.error(`Request Headers:`, ctx.headers);
}
});

3.3 错误处理中间件的位置

错误处理中间件应该放在所有中间件的最前面,这样它才能捕获后续所有中间件产生的错误:

const Koa = require('koa');
const app = new Koa();

// 1. 错误处理中间件(最前面)
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 错误处理逻辑
ctx.status = error.status || 500;
ctx.body = { message: error.message };
ctx.app.emit('error', error, ctx);
}
});

// 2. 其他中间件
app.use(async (ctx, next) => {
console.log(`Request: ${ctx.method} ${ctx.url}`);
await next();
});

// 3. 路由中间件
app.use(router.routes());
app.use(router.allowedMethods());

// 错误事件监听
app.on('error', (error, ctx) => {
console.error('Error:', error);
});

app.listen(3000);

4. 异步错误处理

4.1 处理Promise错误

在Koa中,所有中间件都应该返回Promise,这样Koa才能正确处理异步错误:

// 正确的异步中间件
app.use(async (ctx, next) => {
try {
// 异步操作
const result = await someAsyncOperation();
ctx.body = result;
await next();
} catch (error) {
ctx.throw(500, error.message);
}
});

// 注意:不要这样写,这会导致错误无法被上层中间件捕获
app.use((ctx, next) => {
someAsyncOperation()
.then(result => {
ctx.body = result;
next(); // 这里的错误不会被捕获
})
.catch(error => {
// 错误处理
});
});

4.2 处理回调错误

对于基于回调的API,应该将其转换为Promise,以便在Koa中更好地处理错误:

const fs = require('fs').promises; // Node.js 10+ 提供的Promise API

// 或者使用util.promisify转换回调API
const util = require('util');
const fsCallback = require('fs');
const readFile = util.promisify(fsCallback.readFile);

app.use(async (ctx) => {
try {
// 使用Promise版本的API
const content = await readFile('example.txt', 'utf8');
ctx.body = content;
} catch (error) {
ctx.status = 500;
ctx.body = { message: error.message };
}
});

4.3 处理事件错误

对于基于事件的API,应该监听错误事件并将其转换为Promise的拒绝:

function eventToPromise(emitter, event, errorEvent) {
return new Promise((resolve, reject) => {
emitter.once(event, resolve);
emitter.once(errorEvent || 'error', reject);
});
}

app.use(async (ctx) => {
try {
const emitter = createEventEmitter();
const result = await eventToPromise(emitter, 'data', 'error');
ctx.body = result;
} catch (error) {
ctx.status = 500;
ctx.body = { message: error.message };
}
});

5. 错误对象与类型

5.1 HTTP错误对象

可以创建自定义的HTTP错误类,扩展原生Error类:

// 创建HTTP错误类
class HttpError extends Error {
constructor(status, message, code) {
super(message);
this.name = 'HttpError';
this.status = status;
this.code = code;
this.isHttpError = true;
// 确保正确的堆栈跟踪
Error.captureStackTrace(this, this.constructor);
}
}

// 创建特定错误类型
class BadRequestError extends HttpError {
constructor(message = 'Bad Request', code = 'BAD_REQUEST') {
super(400, message, code);
this.name = 'BadRequestError';
}
}

class UnauthorizedError extends HttpError {
constructor(message = 'Unauthorized', code = 'UNAUTHORIZED') {
super(401, message, code);
this.name = 'UnauthorizedError';
}
}

class NotFoundError extends HttpError {
constructor(message = 'Not Found', code = 'NOT_FOUND') {
super(404, message, code);
this.name = 'NotFoundError';
}
}

class InternalServerError extends HttpError {
constructor(message = 'Internal Server Error', code = 'SERVER_ERROR') {
super(500, message, code);
this.name = 'InternalServerError';
}
}

// 使用自定义错误类
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 处理HTTP错误
if (error.isHttpError) {
ctx.status = error.status;
ctx.body = {
code: error.code,
message: error.message
};
} else {
// 处理其他错误
ctx.status = 500;
ctx.body = {
code: 'SERVER_ERROR',
message: 'Internal Server Error'
};
}
ctx.app.emit('error', error, ctx);
}
});

// 抛出自定义错误
app.use(async ctx => {
if (!ctx.query.apiKey) {
throw new UnauthorizedError('API key is required');
}

const resource = await fetchResource(ctx.query.id);
if (!resource) {
throw new NotFoundError(`Resource with id ${ctx.query.id} not found`);
}

ctx.body = resource;
});

5.2 错误信息格式化

可以创建一个错误格式化工具,统一处理错误响应格式:

// 错误格式化工具
const errorFormatter = {
format: (error) => {
// 默认错误信息
let status = 500;
let code = 'SERVER_ERROR';
let message = 'Internal Server Error';

// 根据错误类型格式化
if (error.isHttpError) {
status = error.status;
code = error.code;
message = error.message;
} else if (error.name === 'ValidationError') {
status = 400;
code = 'VALIDATION_ERROR';
message = error.details.map(detail => detail.message).join(', ');
} else if (error.name === 'SyntaxError') {
status = 400;
code = 'SYNTAX_ERROR';
message = 'Invalid JSON syntax';
} else if (error.message) {
// 其他有message的错误
message = error.message;
}

return {
status,
body: {
code,
message,
// 非生产环境下包含详细错误信息
...(process.env.NODE_ENV !== 'production' && {
stack: error.stack,
originalError: error
})
}
};
}
};

// 在错误处理中间件中使用
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
const formattedError = errorFormatter.format(error);
ctx.status = formattedError.status;
ctx.body = formattedError.body;
ctx.app.emit('error', error, ctx);
}
});

6. 不同环境的错误处理

6.1 开发环境错误处理

在开发环境中,我们需要详细的错误信息以便调试:

app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
ctx.status = error.status || 500;

if (process.env.NODE_ENV === 'development') {
// 开发环境:显示详细错误信息
ctx.body = {
code: error.code || 'DEV_ERROR',
message: error.message,
stack: error.stack,
error: error
};
} else {
// 生产环境:简化错误信息
ctx.body = {
code: error.code || 'SERVER_ERROR',
message: ctx.status === 500 ? 'Internal Server Error' : error.message
};
}

ctx.app.emit('error', error, ctx);
}
});

6.2 生产环境错误处理

在生产环境中,我们应该隐藏详细错误信息,只返回必要的错误提示:

// 生产环境错误处理增强
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 记录详细错误日志
logger.error({
message: error.message,
stack: error.stack,
url: ctx.url,
method: ctx.method,
headers: ctx.headers,
body: ctx.request.body
});

// 设置状态码
ctx.status = error.status || 500;

// 生产环境下的错误响应
if (process.env.NODE_ENV === 'production') {
// 只返回通用错误信息,不暴露系统细节
const safeErrors = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
429: 'Too Many Requests'
};

ctx.body = {
code: error.code || `ERROR_${ctx.status}`,
message: safeErrors[ctx.status] || 'Internal Server Error'
};
} else {
// 开发环境显示详细错误
ctx.body = {
code: error.code || 'DEV_ERROR',
message: error.message,
stack: error.stack
};
}

ctx.app.emit('error', error, ctx);
}
});

7. 集中式错误处理

7.1 创建错误处理服务

在大型应用中,可以创建一个集中式的错误处理服务:

// services/errorService.js
class ErrorService {
constructor(config = {}) {
this.config = {
logger: console, // 默认日志器
notifyOnError: false, // 是否发送错误通知
...config
};
}

// 处理错误
async handleError(error, ctx = null) {
// 记录错误
this.logError(error, ctx);

// 发送错误通知(如果配置了)
if (this.config.notifyOnError && process.env.NODE_ENV === 'production') {
await this.notifyError(error, ctx);
}

// 返回格式化的错误信息
return this.formatError(error);
}

// 记录错误
logError(error, ctx) {
const logData = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
};

// 如果有ctx,添加请求信息
if (ctx) {
logData.request = {
url: ctx.url,
method: ctx.method,
headers: ctx.headers,
body: ctx.request.body,
params: ctx.params,
query: ctx.query,
ip: ctx.ip
};
}

this.config.logger.error('Error occurred:', logData);
}

// 发送错误通知
async notifyError(error, ctx) {
try {
// 这里可以实现发送邮件、短信或推送通知的逻辑
// await notificationService.sendErrorNotification(error, ctx);
console.log('Error notification would be sent here');
} catch (notifyError) {
console.error('Failed to send error notification:', notifyError);
}
}

// 格式化错误
formatError(error) {
let status = 500;
let code = 'SERVER_ERROR';
let message = 'Internal Server Error';

if (error.isHttpError) {
status = error.status;
code = error.code;
message = error.message;
} else if (error.name === 'ValidationError') {
status = 400;
code = 'VALIDATION_ERROR';
message = error.details.map(detail => detail.message).join(', ');
}

// 生产环境下隐藏详细错误信息
if (process.env.NODE_ENV === 'production') {
return {
status,
body: {
code,
message: status === 500 ? 'Internal Server Error' : message
}
};
} else {
return {
status,
body: {
code,
message,
stack: error.stack,
error
}
};
}
}
}

module.exports = ErrorService;

7.2 在中间件中使用错误处理服务

const Koa = require('koa');
const ErrorService = require('./services/errorService');

const app = new Koa();
const errorService = new ErrorService({
logger: console, // 可以替换为自定义日志器
notifyOnError: true // 生产环境下发送错误通知
});

// 全局错误处理中间件
app.use(async (ctx, next) => {
try {
await next();

// 处理404错误
if (ctx.status === 404 && !ctx.body) {
ctx.status = 404;
ctx.body = {
code: 'NOT_FOUND',
message: 'Resource not found'
};
}
} catch (error) {
// 使用错误处理服务处理错误
const formattedError = await errorService.handleError(error, ctx);

// 设置响应
ctx.status = formattedError.status;
ctx.body = formattedError.body;
}
});

// 应用其他中间件和路由...

app.listen(3000);

8. 常见错误场景处理

8.1 数据库错误处理

数据库错误是Web应用中常见的错误类型,应该妥善处理:

// 数据库错误处理中间件
const handleDatabaseError = async (ctx, next) => {
try {
await next();
} catch (error) {
// 检查是否是数据库错误
if (error.code && error.code.startsWith('ER_')) {
// MySQL错误
ctx.status = 500;
ctx.body = {
code: 'DATABASE_ERROR',
message: 'Database error occurred'
};
console.error('Database error:', error);
} else if (error.name === 'MongoError') {
// MongoDB错误
ctx.status = 500;
ctx.body = {
code: 'MONGODB_ERROR',
message: 'Database error occurred'
};
console.error('MongoDB error:', error);
} else {
// 不是数据库错误,继续抛出
throw error;
}
}
};

// 应用数据库错误处理中间件
app.use(handleDatabaseError);

8.2 验证错误处理

使用Joi等验证库时,应该统一处理验证错误:

const Joi = require('joi');

// 验证中间件
const validate = (schema, source = 'body') => {
return async (ctx, next) => {
try {
// 根据source获取要验证的数据
const data = source === 'body' ? ctx.request.body :
source === 'query' ? ctx.query :
source === 'params' ? ctx.params : {};

// 验证数据
await schema.validateAsync(data);
await next();
} catch (error) {
// 处理验证错误
ctx.status = 400;
ctx.body = {
code: 'VALIDATION_ERROR',
message: error.details[0].message,
details: error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}))
};
}
};
};

// 使用验证中间件
const userSchema = Joi.object({
name: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});

app.post('/api/users', validate(userSchema), async ctx => {
// 验证通过后处理请求
const user = await createUser(ctx.request.body);
ctx.status = 201;
ctx.body = user;
});

8.3 超时错误处理

处理请求超时错误:

// 超时处理中间件
const timeoutHandler = (timeoutMs = 30000) => {
return async (ctx, next) => {
// 创建一个Promise来处理超时
const timeoutPromise = new Promise((_, reject) => {
const timeoutId = setTimeout(() => {
const timeoutError = new Error('Request timeout');
timeoutError.status = 408;
timeoutError.code = 'TIMEOUT';
reject(timeoutError);
}, timeoutMs);

// 保存timeoutId到上下文,以便后续清除
ctx.state.timeoutId = timeoutId;
});

try {
// 使用Promise.race同时处理请求和超时
await Promise.race([next(), timeoutPromise]);
} catch (error) {
throw error;
} finally {
// 清除超时定时器
clearTimeout(ctx.state.timeoutId);
}
};
};

// 应用超时处理中间件
app.use(timeoutHandler(30000)); // 30秒超时

// 模拟耗时操作
app.get('/slow-operation', async ctx => {
// 模拟一个耗时5秒的操作
await new Promise(resolve => setTimeout(resolve, 5000));
ctx.body = 'Operation completed';
});

9. 错误监控与日志

9.1 结构化错误日志

使用结构化日志记录错误信息,便于后续分析和查询:

// 简单的日志服务
const logger = {
error: (message, metadata = {}) => {
const logEntry = {
level: 'ERROR',
message,
timestamp: new Date().toISOString(),
...metadata,
// 添加环境信息
environment: process.env.NODE_ENV || 'development',
version: process.env.APP_VERSION || 'unknown'
};

// 在生产环境中,可以将日志发送到日志服务
if (process.env.NODE_ENV === 'production') {
// logService.send(logEntry);
console.error(JSON.stringify(logEntry));
} else {
// 开发环境中直接打印
console.error('ERROR:', message, metadata);
}
},

info: (message, metadata = {}) => {
// 类似的info日志实现
},

debug: (message, metadata = {}) => {
// 类似的debug日志实现
}
};

// 在错误处理中间件中使用
app.on('error', (error, ctx) => {
logger.error('Server error', {
error: {
message: error.message,
stack: error.stack,
name: error.name,
code: error.code,
status: error.status
},
request: ctx ? {
url: ctx.url,
method: ctx.method,
headers: ctx.headers,
body: ctx.request.body,
ip: ctx.ip
} : undefined
});
});

9.2 错误监控服务集成

可以集成第三方错误监控服务,如Sentry、Bugsnag等:

const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');

// 初始化Sentry
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
// 启用HTTP调用跟踪
new Sentry.Integrations.Http({ tracing: true }),
// 启用Express跟踪集成
new Tracing.Integrations.Express({ app }),
],
// 设置采样率
tracesSampleRate: 1.0,
// 环境
environment: process.env.NODE_ENV
});
}

// Sentry错误处理中间件
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

// 应用其他中间件...

// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// 捕获错误并发送到Sentry
Sentry.captureException(error, {
contexts: {
request: {
url: ctx.url,
method: ctx.method,
headers: ctx.headers,
data: ctx.request.body
}
}
});

// 处理错误响应
ctx.status = error.status || 500;
ctx.body = {
code: error.code || 'SERVER_ERROR',
message: process.env.NODE_ENV === 'production' ?
'Internal Server Error' : error.message
};
}
});

// Sentry错误处理
app.use(Sentry.Handlers.errorHandler());

10. 实战案例:构建健壮的错误处理系统

10.1 项目概述

在这个实战案例中,我们将构建一个完整的Koa应用,实现一套健壮的错误处理系统,包括全局错误处理、自定义错误类型、错误日志、验证错误处理等功能。

10.2 技术栈

  • Koa 2.x
  • koa-router
  • koa-bodyparser
  • Joi (数据验证)
  • winston (日志管理)

10.3 项目结构

koa-error-handling/
├── app.js
├── package.json
├── config/
│ └── index.js
├── middlewares/
│ ├── errorHandler.js
│ ├── validator.js
│ └── notFoundHandler.js
├── errors/
│ ├── index.js
│ ├── HttpError.js
│ └── ValidationError.js
├── routes/
│ ├── index.js
│ └── userRoutes.js
├── controllers/
│ └── userController.js
├── services/
│ ├── userService.js
│ └── loggerService.js
└── utils/
└── response.js

10.4 实现核心组件

1. 自定义错误类型 (errors/HttpError.js)

class HttpError extends Error {
constructor(status, message, code) {
super(message);
this.name = this.constructor.name;
this.status = status;
this.code = code;
this.isOperational = true; // 标记为可操作错误
Error.captureStackTrace(this, this.constructor);
}
}

module.exports = HttpError;

2. 验证错误类型 (errors/ValidationError.js)

const HttpError = require('./HttpError');

class ValidationError extends HttpError {
constructor(errors) {
super(400, 'Validation Error', 'VALIDATION_ERROR');
this.errors = errors;
}
}

module.exports = ValidationError;

3. 错误导出文件 (errors/index.js)

const HttpError = require('./HttpError');
const ValidationError = require('./ValidationError');

// 特定HTTP错误
class BadRequestError extends HttpError {
constructor(message = 'Bad Request', code = 'BAD_REQUEST') {
super(400, message, code);
}
}

class UnauthorizedError extends HttpError {
constructor(message = 'Unauthorized', code = 'UNAUTHORIZED') {
super(401, message, code);
}
}

class ForbiddenError extends HttpError {
constructor(message = 'Forbidden', code = 'FORBIDDEN') {
super(403, message, code);
}
}

class NotFoundError extends HttpError {
constructor(message = 'Not Found', code = 'NOT_FOUND') {
super(404, message, code);
}
}

class InternalServerError extends HttpError {
constructor(message = 'Internal Server Error', code = 'SERVER_ERROR') {
super(500, message, code);
}
}

module.exports = {
HttpError,
ValidationError,
BadRequestError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
InternalServerError
};

4. 日志服务 (services/loggerService.js)

const winston = require('winston');

// 创建日志记录器
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: {
service: 'koa-app',
environment: process.env.NODE_ENV
},
transports: [
// 错误日志文件
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// 所有日志文件
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});

// 开发环境下,同时输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}

module.exports = logger;

5. 验证中间件 (middlewares/validator.js)

const Joi = require('joi');
const { ValidationError } = require('../errors');

const validator = (schema, source = 'body') => {
return async (ctx, next) => {
try {
const data = source === 'body' ? ctx.request.body :
source === 'query' ? ctx.query :
source === 'params' ? ctx.params : {};

// 验证数据
await schema.validateAsync(data, {
abortEarly: false // 返回所有错误,而不是第一个
});

await next();
} catch (error) {
// 转换为自定义验证错误
if (error.isJoi) {
const validationErrors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
type: detail.type
}));

throw new ValidationError(validationErrors);
}

throw error;
}
};
};

module.exports = validator;

6. 404处理中间件 (middlewares/notFoundHandler.js)

const { NotFoundError } = require('../errors');

module.exports = async (ctx) => {
throw new NotFoundError(`Cannot ${ctx.method} ${ctx.url}`);
};

7. 错误处理中间件 (middlewares/errorHandler.js)

const logger = require('../services/loggerService');
const { InternalServerError } = require('../errors');

module.exports = async (ctx, next) => {
try {
await next();
} catch (error) {
// 记录错误信息
const errorContext = {
request: {
url: ctx.url,
method: ctx.method,
headers: ctx.headers,
body: ctx.request.body,
ip: ctx.ip
},
error: {
message: error.message,
stack: error.stack,
name: error.name,
code: error.code,
status: error.status
}
};

// 根据错误类型记录不同级别的日志
if (error.isOperational) {
// 可操作错误,记录为warn级别
logger.warn(error.message, errorContext);
} else {
// 不可操作错误,记录为error级别
logger.error('Unhandled error', errorContext);
}

// 设置响应状态码和响应体
ctx.status = error.status || 500;

// 错误响应格式
const errorResponse = {
code: error.code || 'SERVER_ERROR',
message: error.message || 'Internal Server Error'
};

// 非生产环境下,添加详细错误信息
if (process.env.NODE_ENV !== 'production') {
errorResponse.details = error.errors || null;
errorResponse.stack = error.stack;
}

// 验证错误,添加详细错误字段
if (error.name === 'ValidationError' && error.errors) {
errorResponse.details = error.errors;
}

ctx.body = errorResponse;
}
};

8. 用户控制器 (controllers/userController.js)

const { NotFoundError, BadRequestError } = require('../errors');
const userService = require('../services/userService');

const userController = {
// 获取用户列表
getAllUsers: async (ctx) => {
const users = await userService.getAllUsers();
ctx.body = {
code: 200,
message: 'Success',
data: users
};
},

// 获取单个用户
getUserById: async (ctx) => {
const user = await userService.getUserById(ctx.params.id);
if (!user) {
throw new NotFoundError(`User with ID ${ctx.params.id} not found`);
}

ctx.body = {
code: 200,
message: 'Success',
data: user
};
},

// 创建用户
createUser: async (ctx) => {
// 验证在中间件中处理
const newUser = await userService.createUser(ctx.request.body);

ctx.status = 201;
ctx.body = {
code: 201,
message: 'User created',
data: newUser
};
},

// 更新用户
updateUser: async (ctx) => {
const updatedUser = await userService.updateUser(ctx.params.id, ctx.request.body);
if (!updatedUser) {
throw new NotFoundError(`User with ID ${ctx.params.id} not found`);
}

ctx.body = {
code: 200,
message: 'User updated',
data: updatedUser
};
},

// 删除用户
deleteUser: async (ctx) => {
const deletedUser = await userService.deleteUser(ctx.params.id);
if (!deletedUser) {
throw new NotFoundError(`User with ID ${ctx.params.id} not found`);
}

ctx.body = {
code: 200,
message: 'User deleted'
};
}
};

module.exports = userController;

9. 用户服务 (services/userService.js)

// 模拟数据库
let users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
];
let nextId = 3;

const userService = {
// 获取所有用户
getAllUsers: async () => {
return users;
},

// 获取单个用户
getUserById: async (id) => {
return users.find(user => user.id === parseInt(id));
},

// 创建用户
createUser: async (userData) => {
// 检查邮箱是否已存在
const existingUser = users.find(user => user.email === userData.email);
if (existingUser) {
// 这里会被控制器的错误处理捕获
throw new Error('Email already exists');
}

const newUser = {
id: nextId++,
...userData
};

users.push(newUser);
return newUser;
},

// 更新用户
updateUser: async (id, userData) => {
const userIndex = users.findIndex(user => user.id === parseInt(id));
if (userIndex === -1) {
return null;
}

// 检查更新的邮箱是否已被其他用户使用
if (userData.email) {
const existingUser = users.find(
user => user.email === userData.email && user.id !== parseInt(id)
);
if (existingUser) {
throw new Error('Email already exists');
}
}

users[userIndex] = { ...users[userIndex], ...userData };
return users[userIndex];
},

// 删除用户
deleteUser: async (id) => {
const userIndex = users.findIndex(user => user.id === parseInt(id));
if (userIndex === -1) {
return null;
}

return users.splice(userIndex, 1)[0];
}
};

module.exports = userService;

10. 用户路由 (routes/userRoutes.js)

const Router = require('koa-router');
const Joi = require('joi');
const userController = require('../controllers/userController');
const validator = require('../middlewares/validator');

const router = new Router({ prefix: '/api/users' });

// 用户验证模式
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});

const updateUserSchema = Joi.object({
name: Joi.string().min(2).max(50),
email: Joi.string().email(),
password: Joi.string().min(6)
}).min(1); // 至少需要一个字段

// 路由定义
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', validator(userSchema), userController.createUser);
router.put('/:id', validator(updateUserSchema), userController.updateUser);
router.delete('/:id', userController.deleteUser);

module.exports = router;

11. 路由整合 (routes/index.js)

const Router = require('koa-router');
const userRoutes = require('./userRoutes');

const router = new Router();

// 挂载子路由
router.use(userRoutes.routes(), userRoutes.allowedMethods());

module.exports = router;

12. 应用入口 (app.js)

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');

// 中间件
const errorHandler = require('./middlewares/errorHandler');
const notFoundHandler = require('./middlewares/notFoundHandler');

// 路由
const routes = require('./routes');

// 配置环境变量
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

// 创建应用
const app = new Koa();
const PORT = process.env.PORT || 3000;

// 应用中间件 - 顺序很重要
app.use(errorHandler); // 错误处理中间件放在最前面
app.use(bodyParser());

// 应用路由
app.use(routes.routes());
app.use(routes.allowedMethods());

// 404处理中间件 - 放在所有路由后面
app.use(notFoundHandler);

// 启动服务器
const server = app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT} in ${process.env.NODE_ENV} mode`);
});

// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// 在生产环境中,可能需要优雅地关闭服务器
if (process.env.NODE_ENV === 'production') {
setTimeout(() => process.exit(1), 1000);
}
});

// 处理未处理的Promise拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// 在生产环境中,可能需要优雅地关闭服务器
if (process.env.NODE_ENV === 'production') {
setTimeout(() => process.exit(1), 1000);
}
});

module.exports = server;

10.5 测试错误处理系统

使用curl或Postman测试以下场景:

  1. 验证错误

    • POST /api/users - 不提供必要字段
    • POST /api/users - 提供无效的邮箱格式
  2. 业务逻辑错误

    • GET /api/users/999 - 获取不存在的用户
    • POST /api/users - 使用已存在的邮箱
  3. 404错误

    • GET /api/nonexistent - 访问不存在的路由
  4. 服务器错误

    • 可以在代码中故意引入错误进行测试

11. 总结与进阶建议

错误处理是构建健壮Web应用的关键环节。在Koa中,我们可以利用其基于Promise的特性,构建一套完整的错误处理系统,包括全局错误处理中间件、自定义错误类型、错误日志记录、错误监控等功能。

最佳实践回顾

  • 始终使用try/catch和await处理异步错误
  • 创建全局错误处理中间件,并将其放在所有中间件的最前面
  • 定义自定义错误类型,便于错误分类和处理
  • 为不同环境(开发/生产)提供不同的错误响应格式
  • 记录详细的错误日志,便于调试和问题排查
  • 集成错误监控服务,及时发现和解决问题
  • 为常见错误场景(如数据库错误、验证错误)提供专门的处理逻辑

进阶学习建议

  • 深入学习Promise和async/await的错误处理机制
  • 研究Node.js的事件循环和错误处理机制
  • 学习分布式系统中的错误处理策略
  • 探索微服务架构下的错误跟踪和处理
  • 研究混沌工程,主动测试系统的错误恢复能力

通过不断学习和实践,你将能够构建更加健壮、可靠的Koa应用程序,为用户提供更好的服务体验。