错误处理中间件 #

一、错误处理概述 #

1.1 什么是错误处理中间件? #

错误处理中间件是Express中一种特殊的中间件,它有四个参数而不是三个:

javascript
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('出错了!');
});

1.2 错误处理中间件特点 #

特点 说明
四个参数 err, req, res, next
必须最后定义 放在所有路由之后
捕获所有错误 同步和异步错误
可以有多个 按顺序执行

二、基本用法 #

2.1 捕获同步错误 #

javascript
app.get('/error', (req, res) => {
    throw new Error('出错了!');
});

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: err.message });
});

2.2 捕获异步错误 #

javascript
app.get('/async-error', async (req, res, next) => {
    try {
        const data = await someAsyncFunction();
        res.json(data);
    } catch (error) {
        next(error);
    }
});

app.use((err, req, res, next) => {
    res.status(500).json({ error: err.message });
});

2.3 Express 5.x 异步错误 #

Express 5.x 会自动捕获异步错误:

javascript
app.get('/async-error', async (req, res) => {
    const data = await someAsyncFunction();
    res.json(data);
});

三、自定义错误类 #

3.1 创建错误类 #

javascript
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
        this.isOperational = true;
        
        Error.captureStackTrace(this, this.constructor);
    }
}

class NotFoundError extends AppError {
    constructor(message = '资源未找到') {
        super(message, 404);
    }
}

class UnauthorizedError extends AppError {
    constructor(message = '未授权') {
        super(message, 401);
    }
}

class ForbiddenError extends AppError {
    constructor(message = '禁止访问') {
        super(message, 403);
    }
}

class ValidationError extends AppError {
    constructor(message = '验证失败') {
        super(message, 400);
    }
}

module.exports = {
    AppError,
    NotFoundError,
    UnauthorizedError,
    ForbiddenError,
    ValidationError
};

3.2 使用自定义错误 #

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

app.get('/users/:id', async (req, res, next) => {
    try {
        const user = await User.findById(req.params.id);
        
        if (!user) {
            throw new NotFoundError('用户不存在');
        }
        
        res.json(user);
    } catch (error) {
        next(error);
    }
});

app.get('/protected', (req, res, next) => {
    if (!req.user) {
        throw new UnauthorizedError('请先登录');
    }
    res.json({ user: req.user });
});

四、全局错误处理 #

4.1 基本错误处理 #

javascript
app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    
    res.status(err.statusCode).json({
        status: err.status,
        message: err.message
    });
});

4.2 开发环境错误处理 #

javascript
const sendErrorDev = (err, res) => {
    res.status(err.statusCode).json({
        status: err.status,
        error: err,
        message: err.message,
        stack: err.stack
    });
};

const sendErrorProd = (err, res) => {
    if (err.isOperational) {
        res.status(err.statusCode).json({
            status: err.status,
            message: err.message
        });
    } else {
        console.error('ERROR:', err);
        res.status(500).json({
            status: 'error',
            message: '服务器内部错误'
        });
    }
};

app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    
    if (process.env.NODE_ENV === 'development') {
        sendErrorDev(err, res);
    } else {
        sendErrorProd(err, res);
    }
});

4.3 完整错误处理中间件 #

javascript
const errorHandler = (err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    
    if (process.env.NODE_ENV === 'development') {
        return res.status(err.statusCode).json({
            status: err.status,
            error: err,
            message: err.message,
            stack: err.stack
        });
    }
    
    if (err.name === 'CastError') {
        const message = `无效的 ${err.path}: ${err.value}`;
        err = new AppError(message, 400);
    }
    
    if (err.code === 11000) {
        const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
        const message = `重复的值: ${value},请使用其他值`;
        err = new AppError(message, 400);
    }
    
    if (err.name === 'ValidationError') {
        const errors = Object.values(err.errors).map(el => el.message);
        const message = `无效的输入数据: ${errors.join('. ')}`;
        err = new AppError(message, 400);
    }
    
    if (err.name === 'JsonWebTokenError') {
        err = new AppError('无效的token,请重新登录', 401);
    }
    
    if (err.name === 'TokenExpiredError') {
        err = new AppError('token已过期,请重新登录', 401);
    }
    
    if (err.isOperational) {
        return res.status(err.statusCode).json({
            status: err.status,
            message: err.message
        });
    }
    
    console.error('ERROR:', err);
    return res.status(500).json({
        status: 'error',
        message: '服务器内部错误'
    });
};

module.exports = errorHandler;

五、异步错误处理 #

5.1 asyncHandler包装器 #

javascript
const asyncHandler = fn => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

5.2 使用asyncHandler #

javascript
const asyncHandler = require('../utils/asyncHandler');

app.get('/users', asyncHandler(async (req, res) => {
    const users = await User.find();
    res.json(users);
}));

app.post('/users', asyncHandler(async (req, res) => {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
}));

5.3 全局异步错误捕获 #

javascript
const express = require('express');
const app = express();

const asyncHandler = fn => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

const routes = [
    { method: 'get', path: '/users', handler: async (req, res) => {
        const users = await User.find();
        res.json(users);
    }},
    { method: 'post', path: '/users', handler: async (req, res) => {
        const user = new User(req.body);
        await user.save();
        res.status(201).json(user);
    }}
];

routes.forEach(route => {
    app[route.method](route.path, asyncHandler(route.handler));
});

六、404错误处理 #

6.1 基本404处理 #

javascript
app.use((req, res, next) => {
    res.status(404).json({
        status: 'fail',
        message: `找不到 ${req.originalUrl}`
    });
});

6.2 使用自定义错误 #

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

app.use((req, res, next) => {
    next(new NotFoundError(`找不到 ${req.originalUrl}`));
});

6.3 完整错误处理流程 #

javascript
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello');
});

app.use((req, res, next) => {
    next(new NotFoundError(`找不到 ${req.originalUrl}`));
});

app.use((err, req, res, next) => {
    res.status(err.statusCode || 500).json({
        status: err.status || 'error',
        message: err.message
    });
});

七、错误日志记录 #

7.1 基本日志 #

javascript
app.use((err, req, res, next) => {
    console.error('Error:', err.message);
    console.error('Stack:', err.stack);
    console.error('Request:', req.method, req.originalUrl);
    
    res.status(err.statusCode || 500).json({
        status: 'error',
        message: err.message
    });
});

7.2 使用winston日志 #

bash
npm install winston
javascript
const winston = require('winston');

const logger = winston.createLogger({
    level: 'error',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.Console()
    ]
});

app.use((err, req, res, next) => {
    logger.error({
        message: err.message,
        stack: err.stack,
        method: req.method,
        url: req.originalUrl,
        body: req.body,
        query: req.query,
        params: req.params,
        user: req.user?.id
    });
    
    res.status(err.statusCode || 500).json({
        status: 'error',
        message: err.message
    });
});

八、错误处理最佳实践 #

8.1 统一错误响应格式 #

javascript
{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "验证失败",
        "details": [
            { "field": "email", "message": "请输入有效的邮箱地址" }
        ]
    }
}

8.2 错误码定义 #

javascript
const ERROR_CODES = {
    VALIDATION_ERROR: 'VALIDATION_ERROR',
    NOT_FOUND: 'NOT_FOUND',
    UNAUTHORIZED: 'UNAUTHORIZED',
    FORBIDDEN: 'FORBIDDEN',
    INTERNAL_ERROR: 'INTERNAL_ERROR'
};

class AppError extends Error {
    constructor(message, statusCode, code = ERROR_CODES.INTERNAL_ERROR) {
        super(message);
        this.statusCode = statusCode;
        this.code = code;
        this.isOperational = true;
    }
}

8.3 完整错误处理示例 #

javascript
const express = require('express');
const app = express();

app.use(express.json());

class AppError extends Error {
    constructor(message, statusCode, code) {
        super(message);
        this.statusCode = statusCode;
        this.code = code || 'ERROR';
        this.isOperational = true;
    }
}

const asyncHandler = fn => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/users/:id', asyncHandler(async (req, res) => {
    const user = await User.findById(req.params.id);
    
    if (!user) {
        throw new AppError('用户不存在', 404, 'NOT_FOUND');
    }
    
    res.json(user);
}));

app.use((req, res, next) => {
    next(new AppError(`找不到 ${req.originalUrl}`, 404, 'NOT_FOUND'));
});

app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.code = err.code || 'INTERNAL_ERROR';
    
    const response = {
        success: false,
        error: {
            code: err.code,
            message: err.message
        }
    };
    
    if (process.env.NODE_ENV === 'development') {
        response.error.stack = err.stack;
    }
    
    if (err.errors) {
        response.error.details = err.errors;
    }
    
    console.error(`[${new Date().toISOString()}] ${err.code}: ${err.message}`);
    
    res.status(err.statusCode).json(response);
});

module.exports = app;

九、总结 #

错误处理中间件要点:

概念 说明
四个参数 err, req, res, next
自定义错误 继承Error类
异步错误 使用asyncHandler
404处理 放在路由之后
日志记录 记录错误详情

下一步,让我们学习常用中间件!

最后更新:2026-03-28