阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 错误处理与日志记录策略

错误处理与日志记录策略

作者:陈川 阅读数:29793人阅读 分类: Node.js

错误处理中间件的基本用法

Express中的错误处理中间件与其他中间件类似,但需要接收四个参数:err, req, res, next。错误处理中间件应该定义在所有路由和其他中间件之后。

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

当在路由或中间件中调用next(err)时,Express会跳过所有剩余的非错误处理中间件,直接执行错误处理中间件。

同步与异步错误处理

Express默认可以捕获同步代码中的错误,但对于异步代码,需要手动将错误传递给next()函数。

同步错误处理示例:

app.get('/sync-error', (req, res) => {
  throw new Error('同步错误示例');
});

异步错误处理示例:

app.get('/async-error', async (req, res, next) => {
  try {
    await someAsyncOperation();
  } catch (err) {
    next(err); // 必须手动传递错误
  }
});

自定义错误类

创建自定义错误类可以更好地组织和管理不同类型的错误:

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);
  }
}

// 使用示例
app.get('/custom-error', (req, res, next) => {
  next(new AppError('自定义错误消息', 404));
});

日志记录策略

基本日志记录

使用morgan中间件记录HTTP请求日志:

const morgan = require('morgan');
app.use(morgan('combined')); // 使用预定义的日志格式

自定义日志格式

app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

日志文件存储

使用winston库实现更强大的日志记录:

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

// 在错误处理中使用
app.use((err, req, res, next) => {
  logger.error(err.stack);
  res.status(500).send('Something broke!');
});

错误分类与处理

操作错误与编程错误

操作错误是预期中的错误情况,如无效的用户输入。编程错误是代码中的bug,如未定义的变量。

// 操作错误示例
app.get('/user/:id', (req, res, next) => {
  const user = getUserById(req.params.id);
  if (!user) {
    return next(new AppError('用户不存在', 404));
  }
  res.json(user);
});

// 编程错误示例
app.get('/bug', (req, res) => {
  console.log(undefinedVariable); // 未定义的变量
});

开发环境与生产环境的错误处理

开发环境需要详细的错误堆栈,生产环境则应该返回简化的错误信息。

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

  if (process.env.NODE_ENV === 'development') {
    res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  } else {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message
    });
  }
});

全局未捕获异常处理

处理未捕获的异常和未处理的Promise拒绝:

process.on('uncaughtException', err => {
  console.error('未捕获的异常:', err);
  process.exit(1);
});

process.on('unhandledRejection', err => {
  console.error('未处理的Promise拒绝:', err);
});

请求验证与错误处理

使用express-validator进行请求验证:

const { body, validationResult } = require('express-validator');

app.post('/user', 
  body('email').isEmail(),
  body('password').isLength({ min: 5 }),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return next(new AppError('验证失败', 400, { errors: errors.array() }));
    }
    // 处理有效请求
  }
);

性能监控与错误关联

使用APM工具如elastic-apm-node将错误与性能数据关联:

const apm = require('elastic-apm-node').start({
  serviceName: 'my-express-app',
  serverUrl: 'http://localhost:8200'
});

app.use((err, req, res, next) => {
  apm.captureError(err);
  next(err);
});

日志聚合与分析

配置日志聚合系统如ELK Stack:

const { createLogger, transports } = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');

const esTransport = new ElasticsearchTransport({
  level: 'info',
  clientOpts: { node: 'http://localhost:9200' }
});

const logger = createLogger({
  transports: [esTransport]
});

安全相关的错误处理

处理安全相关的错误,如CSRF令牌无效:

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.post('/process', csrfProtection, (req, res, next) => {
  // 正常处理
}, (err, req, res, next) => {
  if (err.code !== 'EBADCSRFTOKEN') return next(err);
  res.status(403).json({ error: 'CSRF令牌无效' });
});

数据库错误处理

处理数据库操作中的特定错误:

app.get('/db-error', async (req, res, next) => {
  try {
    const result = await db.query('SELECT * FROM non_existent_table');
    res.json(result);
  } catch (err) {
    if (err.code === '42P01') { // PostgreSQL表不存在错误代码
      next(new AppError('请求的资源不存在', 404));
    } else {
      next(err);
    }
  }
});

错误通知系统

集成错误通知服务如Sentry:

const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'https://example@sentry.io/1234567' });

app.use(Sentry.Handlers.errorHandler());

// 自定义错误处理
app.use((err, req, res, next) => {
  Sentry.captureException(err);
  next(err);
});

测试中的错误处理

编写测试时验证错误处理:

const request = require('supertest');
const app = require('../app');

describe('错误处理', () => {
  it('应该处理404错误', async () => {
    const res = await request(app).get('/non-existent-route');
    expect(res.statusCode).toEqual(404);
    expect(res.body.message).toMatch(/找不到/);
  });

  it('应该处理500错误', async () => {
    const res = await request(app).get('/trigger-error');
    expect(res.statusCode).toEqual(500);
    expect(res.body.message).toMatch(/出错了/);
  });
});

客户端错误处理

在响应中包含错误详细信息,帮助客户端正确处理:

app.use((err, req, res, next) => {
  const response = {
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message,
      details: err.details
    },
    links: {
      documentation: 'https://api.example.com/docs/errors'
    }
  };
  
  if (process.env.NODE_ENV === 'development') {
    response.error.stack = err.stack;
  }
  
  res.status(err.statusCode || 500).json(response);
});

错误处理中间件的组织

将错误处理逻辑组织到单独的文件中:

// errorController.js
exports.handleErrors = (err, req, res, next) => {
  // 错误处理逻辑
};

// app.js
const { handleErrors } = require('./controllers/errorController');
app.use(handleErrors);

性能考虑

错误处理中的性能优化:

app.use((err, req, res, next) => {
  // 避免在错误处理中进行耗时的同步操作
  setImmediate(() => {
    logger.error(err.stack);
  });
  
  // 快速响应客户端
  res.status(500).json({ error: 'Internal Server Error' });
});

多语言错误消息

支持多语言错误消息:

const i18n = require('i18n');

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

错误处理与API版本控制

在不同API版本中处理错误:

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// 版本特定的错误处理
app.use('/api/v1', (err, req, res, next) => {
  res.status(err.statusCode || 500).json({
    error: err.message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
  });
});

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

错误处理中间件的测试

测试错误处理中间件:

const testErrorHandler = require('./middleware/errorHandler');
const mockRequest = (params = {}) => ({ params });
const mockResponse = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};

test('应该处理AppError', () => {
  const req = mockRequest();
  const res = mockResponse();
  const next = jest.fn();
  const err = new AppError('测试错误', 400);
  
  testErrorHandler(err, req, res, next);
  
  expect(res.status).toHaveBeenCalledWith(400);
  expect(res.json).toHaveBeenCalledWith({
    status: 'fail',
    message: '测试错误'
  });
});

日志轮转与归档

配置日志轮转避免日志文件过大:

const { createLogger, transports, format } = require('winston');
const { combine, timestamp, printf } = format;
const DailyRotateFile = require('winston-daily-rotate-file');

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} [${level}]: ${message}`;
});

const logger = createLogger({
  format: combine(timestamp(), logFormat),
  transports: [
    new DailyRotateFile({
      filename: 'application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      zippedArchive: true,
      maxSize: '20m',
      maxFiles: '14d'
    })
  ]
});

错误处理与GraphQL

在GraphQL中处理错误:

const { ApolloServer } = require('apollo-server-express');
const formatError = (err) => {
  if (err.originalError instanceof AppError) {
    return {
      message: err.message,
      code: err.originalError.code,
      locations: err.locations,
      path: err.path
    };
  }
  return {
    message: 'Internal server error',
    code: 'INTERNAL_SERVER_ERROR'
  };
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError
});

错误处理与WebSocket

处理WebSocket连接中的错误:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  ws.on('error', (err) => {
    console.error('WebSocket错误:', err);
    ws.send(JSON.stringify({
      type: 'error',
      message: '处理请求时出错'
    }));
  });
});

错误处理与文件上传

处理文件上传中的错误:

const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('file'), (req, res) => {
  // 处理文件
}, (err, req, res, next) => {
  if (err.code === 'LIMIT_FILE_SIZE') {
    return res.status(413).json({ error: '文件太大' });
  }
  next(err);
});

错误处理与身份验证

处理身份验证相关的错误:

const passport = require('passport');

app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) return next(err);
    if (!user) {
      return next(new AppError(info.message || '认证失败', 401));
    }
    // 登录成功
  })(req, res, next);
});

错误处理与限流

处理限流相关的错误:

const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  handler: (req, res, next) => {
    next(new AppError('请求过多,请稍后再试', 429));
  }
});

app.use(limiter);

错误处理与缓存

处理缓存相关的错误:

const redis = require('redis');
const client = redis.createClient();

client.on('error', (err) => {
  logger.error('Redis错误:', err);
  // 可以降级处理或使用其他缓存方案
});

app.get('/cached-data', (req, res, next) => {
  client.get('someKey', (err, reply) => {
    if (err) {
      // 从数据库获取数据作为回退
      return getDataFromDB()
        .then(data => res.json(data))
        .catch(next);
    }
    res.json(JSON.parse(reply));
  });
});

错误处理与队列系统

处理队列系统中的错误:

const { Worker } = require('bullmq');

const worker = new Worker('emailQueue', async job => {
  // 处理任务
}, {
  connection: { host: 'localhost', port: 6379 },
  limiter: { max: 10, duration: 1000 }
});

worker.on('failed', (job, err) => {
  logger.error(`任务 ${job.id} 失败:`, err);
  // 可以重试或通知管理员
});

错误处理与定时任务

处理定时任务中的错误:

const cron = require('node-cron');

cron.schedule('* * * * *', () => {
  try {
    // 执行定时任务
  } catch (err) {
    logger.error('定时任务执行失败:', err);
    // 可以发送通知或记录详细错误
  }
});

错误处理与第三方API调用

处理调用第三方API时的错误:

const axios = require('axios');

app.get('/external-api', async (req, res, next) => {
  try {
    const response = await axios.get('https://api.example.com/data');
    res.json(response.data);
  } catch (err) {
    if (err.response) {
      // 第三方API返回了错误响应
      next(new AppError(`第三方API错误: ${err.response.statusText}`, err.response.status));
    } else if (err.request) {
      // 请求已发出但没有收到响应
      next(new AppError('无法连接到第三方API', 502));
    } else {
      // 设置请求时出错
      next(new AppError('处理请求时出错', 500));
    }
  }
});

错误处理与文件系统操作

处理文件系统操作中的错误:

const fs = require('fs').promises;

app.get('/read-file', async (req, res, next) => {
  try {
    const data = await fs.readFile('somefile.txt', 'utf8');
    res.send(data);
  } catch (err) {
    if (err.code === 'ENOENT') {
      next(new AppError('文件不存在', 404));
    } else {
      next(err);
    }
  }
});

错误处理与子进程

处理子进程中的错误:

const { exec } = require('child_process');

app.get('/run-script', (req, res, next) => {
  exec('some-script.sh', (error, stdout, stderr) => {
    if (error) {
      logger.error(`脚本执行错误: ${error.message}`);
      return next(new AppError('脚本执行失败', 500));
    }
    if (stderr) {
      logger.warn(`脚本输出到stderr: ${stderr}`);
    }
    res.send(stdout);
  });
});

错误处理与数据库连接

处理数据库连接错误:

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/db', {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).catch(err => {
  logger.error('数据库连接失败:', err);
  process.exit(1); // 如果数据库连接失败,通常应该终止应用
});

mongoose.connection.on('error', err => {
  logger.error('数据库错误:', err);
});

错误处理与内存泄漏

监控和处理内存泄漏:

const heapdump = require('heapdump');

process.on('uncaughtException', err => {
  if (err.message.includes('JavaScript heap out of memory')) {
    const filename = `heapdump-${Date.now()}.heapsnapshot`;
    heapdump.writeSnapshot(filename, () => {
      logger.error(`内存不足,已创建堆转储文件: ${filename}`);
      process.exit(1);
    });
  }

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌