Node.js Express中间件异常处理:统一错误响应与日志记录最佳实践

GreenWizard
GreenWizard 2026-03-13T23:05:06+08:00
0 0 0

引言

在现代Web应用开发中,错误处理是确保应用稳定性和用户体验的关键环节。Node.js Express框架作为最受欢迎的后端开发框架之一,其强大的中间件机制为错误处理提供了灵活而强大的支持。然而,如何有效地处理中间件中的异常、统一错误响应格式、实现完善的日志记录,以及提供优雅的错误恢复机制,仍然是开发者面临的重要挑战。

本文将深入探讨Express框架中中间件异常处理的最佳实践,从基础概念到高级应用,帮助开发者构建健壮、可维护的错误处理系统。

Express中间件异常处理基础

中间件执行机制

在Express中,中间件函数是处理请求和响应的函数。它们按照注册顺序依次执行,每个中间件可以选择是否将控制权传递给下一个中间件(通过调用next()函数)。

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

// 一个简单的中间件示例
app.use((req, res, next) => {
    console.log('请求时间:', new Date());
    next(); // 调用下一个中间件
});

app.use((req, res, next) => {
    console.log('处理请求');
    // 可能抛出异常的代码
    if (Math.random() > 0.5) {
        throw new Error('随机错误');
    }
    next();
});

错误处理中间件

Express中专门用于处理错误的中间件具有特殊的签名:errorHandler(err, req, res, next),其中第一个参数是错误对象。

// 错误处理中间件必须放在所有路由之后
app.use((err, req, res, next) => {
    console.error('错误:', err);
    res.status(500).json({
        error: '服务器内部错误',
        message: err.message
    });
});

统一错误响应格式设计

错误响应结构设计

为了提供一致的API响应格式,我们需要设计统一的错误响应结构。这种结构应该包含错误代码、消息、时间戳等信息。

// 定义统一错误响应格式
class ErrorResponse {
    constructor(statusCode, message, errors = null) {
        this.statusCode = statusCode;
        this.message = message;
        this.errors = errors;
        this.timestamp = new Date().toISOString();
    }
}

// 常见错误类型枚举
const ErrorTypes = {
    VALIDATION_ERROR: 'VALIDATION_ERROR',
    NOT_FOUND: 'NOT_FOUND',
    UNAUTHORIZED: 'UNAUTHORIZED',
    FORBIDDEN: 'FORBIDDEN',
    INTERNAL_ERROR: 'INTERNAL_ERROR'
};

module.exports = { ErrorResponse, ErrorTypes };

实现统一错误响应中间件

const { ErrorResponse, ErrorTypes } = require('./errorResponse');

// 统一错误处理中间件
const errorHandler = (err, req, res, next) => {
    let error = err;
    
    // 如果不是自定义错误对象,创建一个
    if (!(err instanceof ErrorResponse)) {
        error = new ErrorResponse(
            err.statusCode || 500,
            err.message || '服务器内部错误',
            err.errors || null
        );
    }
    
    // 记录错误日志
    logError(error, req);
    
    // 返回统一格式的错误响应
    res.status(error.statusCode).json({
        success: false,
        error: {
            code: error.statusCode,
            message: error.message,
            errors: error.errors,
            timestamp: error.timestamp
        }
    });
};

module.exports = errorHandler;

异常日志记录策略

基础日志记录实现

完善的错误处理需要配套的日志记录系统。我们需要记录错误的详细信息、请求上下文和时间戳。

const fs = require('fs');
const path = require('path');

// 日志配置
const logConfig = {
    errorLogPath: './logs/error.log',
    accessLogPath: './logs/access.log',
    maxSize: '100m',
    maxFiles: 5
};

// 创建日志目录
function ensureLogDirectory() {
    const logDir = path.dirname(logConfig.errorLogPath);
    if (!fs.existsSync(logDir)) {
        fs.mkdirSync(logDir, { recursive: true });
    }
}

// 错误日志记录函数
function logError(error, req) {
    ensureLogDirectory();
    
    const logEntry = {
        timestamp: new Date().toISOString(),
        level: 'ERROR',
        message: error.message,
        stack: error.stack,
        url: req.url,
        method: req.method,
        ip: req.ip || req.connection.remoteAddress,
        userAgent: req.get('User-Agent'),
        headers: req.headers,
        body: req.body,
        params: req.params,
        query: req.query
    };
    
    const logMessage = JSON.stringify(logEntry) + '\n';
    
    // 写入错误日志文件
    fs.appendFileSync(logConfig.errorLogPath, logMessage);
    
    // 同时输出到控制台(用于开发环境)
    if (process.env.NODE_ENV === 'development') {
        console.error('错误日志:', logMessage);
    }
}

module.exports = { logError };

高级日志记录实现

const winston = require('winston');
const expressWinston = require('express-winston');

// 创建winston logger实例
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    defaultMeta: { service: 'user-service' },
    transports: [
        // 错误日志文件
        new winston.transports.File({
            filename: './logs/error.log',
            level: 'error',
            maxsize: '100m',
            maxFiles: 5
        }),
        // 所有日志文件
        new winston.transports.File({
            filename: './logs/combined.log',
            maxsize: '100m',
            maxFiles: 5
        })
    ]
});

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

// Express中间件日志记录
const requestLogger = expressWinston.logger({
    transports: [
        new winston.transports.File({ filename: './logs/access.log' })
    ],
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    expressFormat: true,
    colorize: false
});

module.exports = { logger, requestLogger };

实际应用示例

完整的错误处理系统

const express = require('express');
const app = express();
const errorHandler = require('./middleware/errorHandler');
const { logger, requestLogger } = require('./utils/logger');

// 中间件配置
app.use(requestLogger); // 请求日志记录
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 模拟路由处理
app.get('/user/:id', (req, res, next) => {
    const { id } = req.params;
    
    if (!id) {
        const error = new Error('用户ID不能为空');
        error.statusCode = 400;
        return next(error);
    }
    
    if (id === 'invalid') {
        const error = new Error('用户不存在');
        error.statusCode = 404;
        return next(error);
    }
    
    // 模拟数据库查询
    setTimeout(() => {
        res.json({
            id: id,
            name: `用户${id}`,
            email: `user${id}@example.com`
        });
    }, 100);
});

// 错误处理中间件必须放在所有路由之后
app.use(errorHandler);

// 404处理
app.use((req, res, next) => {
    const error = new Error('请求的资源不存在');
    error.statusCode = 404;
    next(error);
});

// 全局错误处理
app.use((err, req, res, next) => {
    logger.error({
        message: err.message,
        stack: err.stack,
        url: req.url,
        method: req.method,
        ip: req.ip
    });
    
    res.status(err.statusCode || 500).json({
        success: false,
        error: {
            code: err.statusCode || 500,
            message: err.message || '服务器内部错误'
        }
    });
});

module.exports = app;

自定义错误类实现

// 自定义错误类
class CustomError extends Error {
    constructor(message, statusCode, errors = null) {
        super(message);
        this.name = this.constructor.name;
        this.statusCode = statusCode;
        this.errors = errors;
        this.timestamp = new Date().toISOString();
        
        // 保留原始堆栈跟踪
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

// 具体错误类型
class ValidationError extends CustomError {
    constructor(message, errors = null) {
        super(message, 400, errors);
        this.name = 'ValidationError';
    }
}

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

class UnauthorizedError extends CustomError {
    constructor(message = '未授权访问') {
        super(message, 401);
        this.name = 'UnauthorizedError';
    }
}

class ForbiddenError extends CustomError {
    constructor(message = '访问被拒绝') {
        super(message, 403);
        this.name = 'ForbiddenError';
    }
}

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

高级异常处理技巧

异步错误处理

在Express中,异步操作中的错误需要特别处理。使用Promise和async/await时,错误处理机制有所不同。

// 异步错误处理中间件
const asyncHandler = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

// 使用示例
app.get('/async-user/:id', asyncHandler(async (req, res, next) => {
    const { id } = req.params;
    
    // 模拟异步操作
    const user = await findUserById(id);
    
    if (!user) {
        throw new NotFoundError('用户不存在');
    }
    
    res.json(user);
}));

// 错误处理中间件
app.use((err, req, res, next) => {
    console.error('异步错误:', err);
    
    // 根据错误类型返回不同状态码
    if (err instanceof ValidationError) {
        return res.status(400).json({
            success: false,
            error: {
                code: 400,
                message: err.message,
                errors: err.errors
            }
        });
    }
    
    // 其他错误统一处理
    res.status(err.statusCode || 500).json({
        success: false,
        error: {
            code: err.statusCode || 500,
            message: err.message || '服务器内部错误'
        }
    });
});

错误恢复机制

在某些场景下,我们需要实现错误恢复机制,而不是简单地返回错误。

// 错误恢复中间件
const recoveryHandler = (err, req, res, next) => {
    // 记录错误
    logger.error({
        message: err.message,
        stack: err.stack,
        url: req.url,
        method: req.method
    });
    
    // 根据错误类型决定是否恢复
    if (err.statusCode === 400) {
        // 验证错误,可以尝试恢复或提供更好的提示
        return res.status(400).json({
            success: false,
            error: {
                code: 400,
                message: '请求参数验证失败',
                details: err.errors || []
            }
        });
    }
    
    if (err.statusCode === 500) {
        // 服务器错误,尝试返回默认值或重试
        return res.status(500).json({
            success: false,
            error: {
                code: 500,
                message: '服务暂时不可用,请稍后重试'
            }
        });
    }
    
    next(err);
};

// 使用错误恢复中间件
app.use(recoveryHandler);

健康检查与错误监控

// 健康检查端点
app.get('/health', (req, res) => {
    const healthCheck = {
        uptime: process.uptime(),
        message: 'OK',
        timestamp: Date.now(),
        status: 200
    };
    
    // 检查数据库连接等关键服务
    try {
        // 这里可以添加数据库连接检查
        res.status(200).json(healthCheck);
    } catch (error) {
        healthCheck.status = 503;
        healthCheck.message = 'Service Unavailable';
        logger.error('健康检查失败:', error);
        res.status(503).json(healthCheck);
    }
});

// 错误监控中间件
const monitoringMiddleware = (req, res, next) => {
    const start = Date.now();
    
    // 监控响应时间
    res.on('finish', () => {
        const duration = Date.now() - start;
        
        if (duration > 1000) { // 超过1秒的请求记录警告
            logger.warn({
                message: '慢请求',
                url: req.url,
                method: req.method,
                duration: `${duration}ms`,
                ip: req.ip
            });
        }
        
        // 记录成功请求
        if (res.statusCode < 400) {
            logger.info({
                message: '请求成功',
                url: req.url,
                method: req.method,
                statusCode: res.statusCode,
                duration: `${duration}ms`,
                ip: req.ip
            });
        }
    });
    
    next();
};

app.use(monitoringMiddleware);

最佳实践总结

错误处理原则

  1. 统一性:所有错误响应应该遵循相同的格式和结构
  2. 可读性:错误消息应该对开发者友好且易于理解
  3. 安全性:避免在生产环境中暴露敏感的内部错误信息
  4. 完整性:记录足够的上下文信息以便调试

实现建议

// 完整的错误处理模块
const express = require('express');
const app = express();
const { logger } = require('./utils/logger');
const { ErrorResponse, ErrorTypes } = require('./models/errorResponse');

// 通用错误处理中间件
const globalErrorHandler = (err, req, res, next) => {
    // 记录详细错误信息
    const errorInfo = {
        timestamp: new Date().toISOString(),
        url: req.url,
        method: req.method,
        ip: req.ip,
        userAgent: req.get('User-Agent'),
        message: err.message,
        stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
        statusCode: err.statusCode || 500
    };
    
    logger.error(errorInfo);
    
    // 根据环境返回不同级别的错误信息
    const errorResponse = new ErrorResponse(
        err.statusCode || 500,
        process.env.NODE_ENV === 'production' 
            ? '服务器内部错误' 
            : err.message,
        process.env.NODE_ENV === 'development' ? err.errors : null
    );
    
    res.status(errorResponse.statusCode).json({
        success: false,
        error: {
            code: errorResponse.statusCode,
            message: errorResponse.message,
            errors: errorResponse.errors,
            timestamp: errorResponse.timestamp
        }
    });
};

// 优雅的错误恢复
const gracefulErrorHandler = (err, req, res, next) => {
    // 如果是404错误,返回默认响应
    if (err.statusCode === 404) {
        return res.status(404).json({
            success: false,
            error: {
                code: 404,
                message: '请求的资源不存在'
            }
        });
    }
    
    // 如果是验证错误,返回详细错误信息
    if (err.statusCode === 400) {
        return res.status(400).json({
            success: false,
            error: {
                code: 400,
                message: '请求参数验证失败',
                details: err.errors || []
            }
        });
    }
    
    // 其他错误使用全局处理
    next(err);
};

module.exports = {
    globalErrorHandler,
    gracefulErrorHandler
};

结论

Node.js Express框架中的中间件异常处理是一个复杂但至关重要的主题。通过本文的探讨,我们可以看到:

  1. 统一错误响应格式能够提升API的一致性和用户体验
  2. 完善的日志记录策略为问题诊断和系统监控提供了重要支持
  3. 合理的错误恢复机制能够在保证服务稳定性的同时提供更好的用户提示
  4. 遵循最佳实践能够帮助我们构建更加健壮和可维护的应用程序

在实际开发中,建议根据具体业务需求定制错误处理策略,并结合监控工具实现完整的错误追踪和告警系统。通过持续优化错误处理机制,我们可以显著提升应用的稳定性和可靠性,为用户提供更好的服务体验。

记住,好的错误处理不仅仅是返回错误信息,更是整个系统质量的重要体现。通过精心设计的异常处理方案,我们能够构建出既稳定又易于维护的现代Web应用程序。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000