JavaScript Promise - 未处理的失败 promise 的追踪 (5/6)

最初版本的 promise 在失败且没有 rejected handler 时,会默默失败。很多人认为这是一个败笔,之后,JavaScript 运行时对这种情况添加了 console warning;而最终,对未处理的失败 promise 的追踪被添加进 JavaScript 规范。本篇介绍如何追踪未处理的失败的 promise。

统一术语:未处理 指 unhandled,失败指 rejected。

识别未处理的失败 promise

由于 promise 的本质,检测一个失败的 promise 是否已经被处理并非易事:

js 复制代码
const rejected = Promise.reject(2)

// 此时, rejected 未被处理

setTimeout(() => {
    rejected.catch((v) => {
        // rejected 在这里被处理
        console.log(v)
    })
}, 1000)

我们可以在任何时候调用 then() 或者 catch(),所以很难准确知道 promise 何时被处理了。上面的代码中,promise 立即失败,但直到 1 秒之后才被处理。

JavaScript 规范认为:只要一个 promise 调用了 then() 方法,这个 promise 就算是被处理了 ,不管是否提供了相关的 handler (这里调用 then() 包括包括调用 include() finally() ,因为它们底层都是调用的 then())。

每一次对 then() 的调用都会创建一个新的 promise,这个新的 promise 负责调用相关的 handler。

js 复制代码
const p1 = new Promise((resolve, reject) => reject(2))

const p2 = p1.then(v => console.log(v))

上面这两行代码中, p1 promise 被认为是已经处理的,因为它调用了 then() 方法。但 p1 其实是一个失败的 promise,它的失败的状态通过 then() 方法被传递给 p2,而 p2 没有相应的失败的 handler,所以 p1 中的失败就没有被处理。运行时可能会报告 p2 中的失败而丢弃 p1 中的失败,因为运行时并不是追踪所有失败且未处理的 promise,只是追踪链中的最后一个 promise 是否有 handler。

虽然 JavaScript 规范确实指示了如何追踪未处理的失败,但没有指定当发生未处理的失败时运行时应该如何处理。这些细节留给运行时本身,所以不同的运行时 (Node、Deno、Bun) 可能有不同的行为。

追踪浏览器中未处理的失败 promise

浏览器中未处理的失败 promise 的追踪在 HTMl 规范 中做了定义。其要点是通过 globalThis 对象出发两个事件:

  • unhandledrejection:当 promise 失败并且在事件循环的一个回合内没有调用失败 handler 时触发。
  • rejectionhandled:当 promise 失败并且在事件循环的一个回合后调用失败 handler 时触发。

这两个事件被设计为一起使用以准确检测未处理的失败 promise。下面的代码展示了事件的触发时机:

js 复制代码
const reject = Promise.reject(new Error("Oops"))

setTimeout(() => {
    // 2. 此处触发 `rejectionhanled` 事件
  reejcted.catch((reason) => console.error(reason.message))
}, 1000)

// 1. 此处触发 `unhandledrejection` 事件

根据触发时机可以看出,要想准确地检测问题,就需要跟踪触发这些事件的 promise。

unhandledrejectionrejectionhandled 这两个事件都接受一个 event 对象作为参数,包含下列属性:

  • type:事件的名称
  • promise:失败的 promise 对象
  • reason:失败的 promise 的值

通过这些属性我们可以追踪哪些 promise 没有指定失败的 handler:

js 复制代码
const rejected = Promise.reject(new Error("Oops"))

setTimeout(() => {
  // 2. 此处触发 `rejectionhandled` 事件
  rejected.catch((reason) => console.error(reason.message))
}, 1000)

globalThis.onunhandledrejection = (event) => {
  console.log(event.type) // unhandledrejection
  console.log(event.reason.message) // Oops
  console.log(event.promise === rejected) // true
}

globalThis.onrejectionhandled = (event) => {
  console.log(event.type) // reejctionhandled
  console.log(event.reason.message) // Oops
  console.log(event.promise === rejected) // true
}

// 1. 此处触发 `unhandledrejection` 事件
  • 可以直接复制上面的代码在浏览器 console 中执行,查看效果
  • 上面的代码也可以使用 addEventListener 注册事件

报告浏览器中未处理的失败 promise

虽然 unhandledrejectionrejectionhandled 事件有助于识别潜在的问题,但在生产中仅仅依靠它们是不够的。 由于我们不一定要记录每个未处理的失败,因为可能稍后就会添加处理失败的 handler,因此指定一个时间范围是有意义的,我们希望在该时间范围内处理所有失败的 promise。例如,我们可能想记录一分钟内所有未被处理的失败 promise,为此,我们需要跟踪触发了 unhandledrejection 但没有触发 rejectionhandled 的 promise。下面是一种参考方法:

js 复制代码
const possiblyUnhandledRejections = new Map()

// 如果失败的 promise 未被处理,添加进 map
globalThis.onunhandledrejection = (event) => {
  possiblyUnhandledRejections.set(event.promise, event.reason)
}

// 如果失败的 promise 被处理,从 map 中删除
globalThis.onrejectionhandled = (event) => {
  possiblyUnhandledRejections.remove(event.promise)
}

setInterval(() => {
  possiblyUnhandledRejections.forEach((reason, promise) => {
    console.error("Unhandled rejection")
    console.error(promise)
    console.error(reason.message ? reason.message : reason)

    // 此处处理未处理的失败 promise
  })

  possiblyUnhandledRejections.clear()
}, 60000)

这里使用 Map 是因为我们需要定期检查哪些 promise 存在,这不是 WeakMap 能做到的事。

避免浏览器中输出 warning

默认情况下,浏览器对于未处理的失败 promise 会在 console 输出 warning,即使我们监听了上面两个事件也不会改变这点。可以通过在 onunhandledrejection 事件处理函数中添加 event.preventDefault() 来阻止浏览器输出 warning:

js 复制代码
globalThis.onunhandledrejection = event => {
  event.preventDefault()
}

注意,这个操作只会阻止浏览器输出 warning,不影响 rejectionhandled 事件。

处理浏览器中未处理的失败 promise

有个小技巧......

我们可以在 unhandledrejection 事件中处理未处理的 promise,来阻止 rejectionhandled 事件发出:

js 复制代码
globalThis.onunhandledrejection = ({ promise, reason }) => {
  promise.catch(() => {}) // 处理 rejection
}

// 这个永远不会被调用
globalThis.onrejectionhandled = ({ promise }) => console.error(promise)

这里 rejectionhandled 事件永远不会被触发因为在这个事件发出之前 promise 已经被处理了,没理由再触发这个事件了。

注意,除非显式调用了 event.preventDefault(),否则上面的代码同样不会阻止浏览器发出 console warning。

追踪 Node.js 中未处理的失败 promise

Node.js 中不使用 globalThis 对象,而是由 process 对象触发事件,相应的两个事件名称为:

  • unhandledRejection
  • rejectionHandled

这两个事件也不接受 event 对象作为参数:

  • unhandledRejection 接受两个参数,分别为 reason 和 promise
  • rejectionHandled 接受一个参数,即 promise

其它运行时如 Deno、Bun 应该大致相同但略有区别,具体来说就触及到知识盲区了。

相关推荐
wearegogog1237 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars7 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤7 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·7 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°7 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854058 小时前
CSS动效
前端·javascript·css
烛阴8 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪8 小时前
markstream-vue实战踩坑笔记
前端
南村群童欺我老无力.8 小时前
Flutter应用鸿蒙迁移实战:性能优化与渐进式迁移指南
javascript·flutter·ci/cd·华为·性能优化·typescript·harmonyos
C_心欲无痕9 小时前
nginx - 实现域名跳转的几种方式
运维·前端·nginx