Node.js 中如何处理 Uncaught Exception (未捕获异常) 和 Unhandled Promise Rejection (未处理的 Promise 拒绝)?

各位好,欢迎来到今天的“Node.js 异常处理生存指南”讲座。我是你们今天的导游,带大家探索 Node.js 异常处理的奇妙世界,保证让大家满载而归,再也不怕那些神秘莫测的 Uncaught Exception 和 Unhandled Promise Rejection。

准备好了吗? 让我们开始吧!

第一站:认识你的敌人 – Uncaught Exception 和 Unhandled Promise Rejection

首先,我们要搞清楚这两个家伙到底是什么来头。想象一下,你写了一段代码,结果它突然崩溃了,控制台里冒出一堆红字,告诉你出现了 "Uncaught Exception" 或者 "Unhandled Promise Rejection"。 别慌,这并不是世界末日,只是你的代码里出了点小问题,Node.js 正在告诉你。

  • Uncaught Exception (未捕获异常): 这指的是在你的代码里,抛出了一个异常,但是没有任何 try...catch 语句来捕获它。 就像一个不受控制的熊孩子,到处乱跑,最后撞坏了东西。

    function dangerousFunction() {
      throw new Error("Boom!");
    }
    
    dangerousFunction(); // 💥  Uncaught Exception!  程序崩溃!
  • Unhandled Promise Rejection (未处理的 Promise 拒绝): 当一个 Promise 被拒绝 (rejected),但是没有任何 .catch()try...catch (在 async/await 中) 来处理这个拒绝时,就会出现这种情况。 想象一下,你答应给别人一个东西,结果没做到,但是你也没告诉人家为什么。

    function failingPromise() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error("Promise Rejected!"));
        }, 100);
      });
    }
    
    failingPromise(); // 💔  Unhandled Promise Rejection!  默默地失败了。

第二站:为什么我们需要处理它们?

你可能会想,"反正程序都崩溃了,关我什么事?" 错! 不处理这些异常,可能会导致非常严重的后果:

  • 程序崩溃: 最直接的后果,你的 Node.js 应用会直接退出,用户体验极差。
  • 数据丢失: 如果程序在处理重要数据的时候崩溃,可能会导致数据丢失或损坏。
  • 安全漏洞: 某些异常可能包含敏感信息,如果未处理,可能会被恶意利用。
  • 资源泄漏: 未处理的异常可能导致资源(如数据库连接、文件句柄)无法释放,最终导致系统资源耗尽。
  • 服务中断: 在高流量环境下,程序崩溃会导致服务中断,影响大量用户。

所以,处理 Uncaught Exception 和 Unhandled Promise Rejection 是一项非常重要的任务,就像给你的程序装上安全气囊,防止发生严重事故。

第三站:全局异常处理 – 最后的防线

Node.js 提供了两种全局事件,可以用来捕获 Uncaught Exception 和 Unhandled Promise Rejection:

  • process.on('uncaughtException', (err, origin) => { ... });
  • process.on('unhandledRejection', (reason, promise) => { ... });

它们就像你的最后一道防线,当所有其他的异常处理机制都失效时,它们就会派上用场。

代码示例:

process.on('uncaughtException', (err, origin) => {
  console.error(`Caught exception: ${err}n` + `Exception origin: ${origin}`);
  // 可以选择记录日志,清理资源,发送告警,然后退出程序
  process.exit(1); // 强烈建议退出程序,因为程序状态可能已经损坏
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // 同样,可以记录日志,发送告警,然后退出程序
  process.exit(1); // 同样强烈建议退出程序
});

// 模拟一个 Uncaught Exception
setTimeout(() => {
  throw new Error('This error was not caught!');
}, 100);

// 模拟一个 Unhandled Promise Rejection
Promise.reject(new Error('This promise was rejected!'));

重要提示:

  • 永远不要尝试恢复:uncaughtExceptionunhandledRejection 事件触发时,你的程序可能已经处于不稳定状态。 尝试恢复可能会导致更严重的问题。 最佳实践是记录错误,发送告警,然后优雅地退出程序。
  • 使用 process.exit(1) 退出程序可以防止程序继续运行,导致更多问题。 1 表示程序因为错误而退出。
  • 监控和告警: 一定要配置监控系统,以便在发生异常时及时收到告警。 这样你才能第一时间知道程序出了问题。
  • 不要滥用: 全局异常处理应该作为最后的手段,而不是替代 try...catch.catch()。 应该尽可能在代码中处理异常。

第四站:局部异常处理 – 精准打击

除了全局异常处理,我们还应该在代码中使用 try...catch.catch() 来处理可能发生的异常。 这就像给你的代码设置陷阱,捕捉那些试图逃脱的异常。

  • try...catch 用于同步代码的异常处理。

    try {
      // 可能会抛出异常的代码
      const result = JSON.parse(someString);
      console.log('Parsed JSON:', result);
    } catch (error) {
      // 捕获异常
      console.error('Error parsing JSON:', error);
      // 处理异常,例如记录日志,给用户友好的提示
    }
  • .catch() 用于 Promise 的异常处理。

    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        console.log('Data:', data);
      })
      .catch(error => {
        console.error('Error fetching data:', error);
        // 处理异常
      });
  • async/awaittry...catchasync/await 函数中,可以使用 try...catch 来处理 Promise 的异常。

    async function fetchData() {
      try {
        const response = await fetch('/api/data');
        const data = await response.json();
        console.log('Data:', data);
      } catch (error) {
        console.error('Error fetching data:', error);
        // 处理异常
      }
    }
    
    fetchData();

最佳实践:

  • 只捕获你知道如何处理的异常: 不要捕获所有异常,然后什么都不做。 这只会掩盖问题。
  • 记录异常信息: 记录异常信息可以帮助你诊断和修复问题。 包括错误消息、堆栈跟踪、以及发生异常时的上下文信息。
  • 提供友好的错误提示: 如果异常是由于用户输入错误导致的,应该给用户提供清晰的错误提示。
  • 不要吞噬异常: 如果你无法处理异常,应该重新抛出它,让上层代码来处理。
  • 使用合适的错误类型: 使用内置的 Error 对象,或者自定义错误类型,可以提供更清晰的错误信息。

第五站:使用 Domains (已弃用,不推荐使用)

在 Node.js 早期版本中,Domains 模块提供了一种将多个异步操作组合到一个组中,并在其中一个操作抛出异常时捕获该异常的方法。但是,Domains 模块已经过时,不建议使用。 它存在一些问题,例如可能导致资源泄漏,以及与某些异步操作不兼容。

相反,应该使用 async_hooks 来追踪异步操作的上下文,并使用 try...catch.catch() 来处理异常。

第六站:使用 async_hooks 追踪异步上下文

async_hooks 模块提供了一种追踪异步操作的生命周期的方法。 它可以用来追踪 Promise 的创建和解决,以及其他异步操作的开始和结束。 虽然它不能直接用来捕获异常,但它可以帮助你更好地理解异步代码的执行流程,从而更容易地诊断和修复异常。

const async_hooks = require('async_hooks');

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    console.log('Async operation started', asyncId, type, triggerAsyncId);
  },
  before(asyncId) {
    console.log('Async operation about to start', asyncId);
  },
  after(asyncId) {
    console.log('Async operation completed', asyncId);
  },
  destroy(asyncId) {
    console.log('Async operation destroyed', asyncId);
  },
});

hook.enable();

setTimeout(() => {
  console.log('Hello from timeout');
}, 100);

Promise.resolve().then(() => {
  console.log('Hello from promise');
});

第七站:使用错误处理中间件 (Express.js)

如果你在使用 Express.js,可以使用错误处理中间件来集中处理应用程序中的错误。

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

app.get('/api/data', (req, res, next) => {
  // 模拟一个错误
  throw new Error('Something went wrong!');
});

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

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

中间件的工作方式:

  1. 当路由处理函数抛出错误时,Express.js 会将错误传递给错误处理中间件。
  2. 错误处理中间件接收四个参数:err (错误对象), req (请求对象), res (响应对象), next (下一个中间件函数)。
  3. 在错误处理中间件中,你可以记录错误信息,发送告警,或者给用户提供友好的错误提示。
  4. next() 函数用于将错误传递给下一个错误处理中间件。 如果没有下一个错误处理中间件,Express.js 会发送一个默认的错误响应。

最佳实践:

  • 将错误处理中间件放在所有其他中间件之后: 这样可以确保所有路由处理函数抛出的错误都会被捕获。
  • 使用多个错误处理中间件: 可以使用多个错误处理中间件来处理不同类型的错误。
  • 根据环境调整错误处理方式: 在开发环境中,可以显示详细的错误信息,以便调试。 在生产环境中,应该隐藏敏感信息,并给用户提供友好的错误提示。

第八站:工具和库

  • Winston/Morgan: 日志记录库,可以用来记录异常信息和其他有用的信息。
  • Sentry/Bugsnag/Rollbar: 错误跟踪服务,可以用来收集和分析应用程序中的错误。
  • PM2/Nodemon: 进程管理工具,可以用来自动重启崩溃的应用程序。

第九站:总结与回顾

我们今天学习了很多关于 Node.js 异常处理的知识。 让我们来回顾一下:

异常类型 处理方式 最佳实践
Uncaught Exception process.on('uncaughtException', (err, origin) => { ... }); 记录错误,发送告警,然后退出程序 (process.exit(1))。 不要尝试恢复。
Unhandled Promise Rejection process.on('unhandledRejection', (reason, promise) => { ... }); 记录错误,发送告警,然后退出程序 (process.exit(1))。 不要尝试恢复。
同步异常 try...catch 只捕获你知道如何处理的异常。 记录异常信息。 提供友好的错误提示。 不要吞噬异常。 使用合适的错误类型。
Promise 异常 .catch()async/await 中的 try...catch 只捕获你知道如何处理的异常。 记录异常信息。 提供友好的错误提示。 不要吞噬异常。 使用合适的错误类型。
Express.js 错误 错误处理中间件 将错误处理中间件放在所有其他中间件之后。 使用多个错误处理中间件。 根据环境调整错误处理方式。

记住,处理异常是一项持续的任务。 随着你的应用程序不断发展,你需要不断地评估和改进你的异常处理策略。

最后的提示:

  • 测试你的异常处理代码: 确保你的异常处理代码能够正常工作。
  • 保持代码简洁: 避免编写过于复杂的异常处理代码。
  • 持续学习: 关注 Node.js 社区的最新动态,学习新的异常处理技术。

希望今天的讲座对大家有所帮助。 祝大家编写出更加健壮和可靠的 Node.js 应用程序! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注