Back

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

使用 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() 使你的错误处理保持一致,链式调用保持简洁。

常见问题

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

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

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

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

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.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay