12k
All articles

使用 Promise.try 编写更简洁的异步链

Promise.try 可将同步抛出的异常转为 rejection,从而保持异步链的整洁。本文对比多种替代方案,并将其应用于条件数据加载模式。

OpenReplay Team
OpenReplay Team
使用 Promise.try 编写更简洁的异步链

如果你曾经编写过一个 promise 链,其起始函数可能是同步的,也可能是异步的,你可能遇到过一个棘手的问题:.catch() 应该放在哪里?

在 promise 返回之前抛出的同步错误不会被 .catch() 捕获,除非你已经在 promise 上下文中。Promise.try() 优雅地解决了这个问题,它为任何 promise 链提供了一个单一、一致的入口点——无论你调用的函数是同步、异步,还是介于两者之间。

核心要点

  • 在有时返回 promise 的函数内部抛出的同步错误可能完全逃脱 .catch() 的捕获,导致未处理的异常。
  • Promise.try() 立即运行函数,并将同步返回值和同步抛出的错误都包装成一个正确的 promise,为你提供统一的错误处理路径。
  • 与 Promise.resolve(fn()) 不同,它能捕获同步错误。与 Promise.resolve().then(fn) 不同,它立即执行而不会延迟到微任务。
  • 它最适合用于具有混合同步/异步入口点的基于 .then() 的链式调用——而不是作为 async/await 的替代品。

问题:逃脱 .catch() 链的同步错误

考虑一个数据加载器,它根据条件同步从缓存读取或异步从 API 获取:

function loadData(key) {
  const cached = getFromCache(key) // may throw synchronously
  if (cached) return cached
  return fetch(`/api/data/${key}`).then(res => res.json())
}

loadData('user-1')
  .then(data => render(data))
  .catch(err => handleError(err)) // ⚠️ Won't catch sync throws from loadData

如果 getFromCache 同步抛出错误,该错误永远不会被 .catch() 捕获。抛出发生在任何 promise 存在之前,因此它完全逃脱了链式调用,成为一个未处理的异常。

这里还有第二个值得注意的细节:当 loadData 直接返回缓存值(非 thenable 对象)时,对其调用 .then() 也会失败,因为普通值没有 .then() 方法。这个函数本质上是脆弱的——它在一个分支中返回 promise,在另一个分支中返回原始值。Promise.try() 通过始终生成 promise 来解决这两个问题。

Promise.try() 如何解决这个问题

Promise.try(fn) 立即执行提供的函数并将结果包装在 promise 中。如果函数返回普通值,它会用该值 resolve。如果返回 promise,它会采用该 promise。如果同步抛出错误,它会将该错误转换为 rejection。

这为你提供了一个单一入口点,通过同一个 .catch() 处理同步和异步错误:

Promise.try(() => loadData('user-1'))
  .then(data => render(data))
  .catch(err => handleError(err)) // ✅ Catches both sync throws and async rejections

无需特殊处理。无需在开始链式调用之前用 try/catch 包装。一切都按预期通过 .catch() 流转。

它与 Promise.resolve().then(fn) 和 Promise.resolve(fn()) 的区别

这两种模式通常被用作变通方法,但它们在重要方面的行为有所不同。

Promise.resolve(fn()) 立即调用 fn(),在 promise 上下文之外。这里的同步抛出是未捕获的异常,而不是 rejection。

Promise.resolve().then(fn) 将 fn 的执行延迟到微任务。这意味着 fn 不会立即运行——当你需要立即执行时,这可能会导致微妙的时序问题,并使行为变得不太可预测。

Promise.try(fn) 立即运行 fn 并且将任何同步抛出捕获为 rejection。对于启动 promise 链来说,它是三者中最可预测的。

模式立即运行 fn捕获同步抛出
Promise.resolve(fn())✅❌
Promise.resolve().then(fn)❌✅
Promise.try(fn)✅✅

实际前端使用场景

Promise.try() 自然适合行为取决于运行时条件的 JavaScript 异步模式:

工具函数可能同步返回缓存数据或异步获取数据:

Promise.try(() => getUserFromCacheOrAPI(userId))
  .then(updateUI)
  .catch(showErrorBanner)

条件异步工作流,其中验证步骤可能在任何异步工作开始之前抛出错误:

Promise.try(() => {
  validateInput(formData) // throws if invalid
  return submitForm(formData) // returns a promise
})
  .then(handleSuccess)
  .catch(handleError)

浏览器支持和兼容性

Promise.try() 已包含在 ECMAScript 2025 (ES2025) 中,并在 Chrome 128+、Firefox 134+、Safari 18.2+ 和 Node.js 22.7.0+ 中得到支持。你可以在 Can I Use 上验证当前的浏览器支持情况,该网站跟踪主要浏览器和运行时的实现状态。

对于较旧的环境,你可以使用简单的包装器进行 polyfill:

Promise.try = Promise.try || function(fn) {
  return new Promise(resolve => resolve(fn()))
}

这是有效的,因为 new Promise 执行器同步运行,所以 fn() 会立即被调用。如果 fn() 抛出错误,Promise 构造函数会捕获它并将其转换为 rejection。如果 fn() 返回 thenable 对象,resolve 会采用它。

结论

Promise.try() 不是 async/await 的替代品。它是一个小型、专注的工具,用于一种特定情况:当入口点可能同步抛出错误或返回值和 promise 的混合时启动 promise 链。

如果你已经在 async 函数内部,try/catch 块自然地处理这两种情况。但是当你使用 .then() 链时——特别是在具有条件逻辑的工具函数或数据加载器周围——Promise.try() 使你的错误处理保持一致,链式调用保持简洁。

常见问题

我可以在 async 函数中使用 Promise.try() 吗?

可以。如果你传递给 Promise.try() 的函数是 async 的,它会返回一个 promise,Promise.try() 会采用该 promise。它的工作方式与传递任何返回 promise 的函数相同。Promise.try() 的主要优势在于处理可能根本不返回 promise 的函数,或者可能在返回之前抛出错误的函数。

Promise.try() 能替代 async 函数内部的 try/catch 吗?

不能。在 async 函数内部,标准的 try/catch 块已经可以捕获同步抛出和 await 的 rejection。Promise.try() 是为需要安全入口点的 .then() 风格链式调用设计的。如果你已经在使用 async/await,你可能不需要 Promise.try()。

Promise.try() 的 polyfill 在生产环境中使用安全吗?

使用 new Promise(resolve => resolve(fn())) 的常见 polyfill 在功能上等同于原生实现。它立即运行 fn,通过 Promise 构造函数捕获同步抛出,并通过 resolve 采用 thenable 对象。在缺乏原生支持的环境中,它可以安全地用于生产环境。

为什么不直接用 Promise.resolve() 包装初始调用?

Promise.resolve(fn()) 在 promise 上下文之外调用 fn,因此同步抛出会成为未捕获的异常。Promise.resolve().then(fn) 可以捕获抛出,但会将执行延迟到微任务,这意味着 fn 不会立即运行。Promise.try(fn) 是唯一既立即运行 fn 又将同步错误捕获为 rejection 的模式。

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.