如何错误手写 ES2025 新增的 Promise.try() 静态方法

全文速览

欢迎关注 前端情报社。大家好,我是社长林语冰。

Promise 从 ES2015 成为 JavaScript 的一部分。10 年后,ES2025 是第 16 版 JavaScript 语言规范,它新增了 9 种颠覆性功能,Promise.try() 就是其中之一。

Promise.try() 提案并非原创,ES2025 之前,bluebird 和 p-try 等流行库就提供了等价的功能。但我发现 GitHub 上一些遗留源码,为了不使用第三方库,自己会尝试手写模拟实现 Promise.try() 的功能,但部分实现采用了下列错误方案:

  • Promise.resolve() 会导致异常逃逸
  • Promise.prototype.then() 会产生多余的微任务

本文我们会探讨 ES2025 最新 Promise.try() 静态方法的基本用法,以及如何正确手写 Promise.try()

使用场景

Promise.try() 适用于将回调封装为 Promise 风格,然后安全开启链式调用的场景:

js 复制代码
import { readFile } from 'node:fs/promises'

function readLocalFile(path) {
  if (!path) {
    throw new Error('path 不能为空')
  }

  path = new URL(path, import.meta.url)

  return readFile(path, { encoding: 'utf8' })
}

Promise.try(readLocalFile).catch(console.log) // ❌ path 不能为空
Promise.try(readLocalFile, './package.json').then(console.log) // ✅

这里,Promise.try() 会接受一个回调,并将返回值转化为 promise,以便后续开启链式调用。此外,回调内部的同步/异步异常,都会被捕获并转化为失败的 promise 实例。

只有正确掌握 Promise.try() 的行为机制,我们才能正确手写模拟 Promise.try()

异常逃逸

GitHub 上一些遗留代码采用了 ES6 的 Promise.resolve() 来模拟 Promise.try() 的行为,其实是一种错误的方案:

js 复制代码
Promise.try = function promiseTry(fn, ...args) {
  return Promise.resolve(fn(...args))
}

Promise.try(readLocalFile, './package.json').then(console.log) // ✅
Promise.try(readLocalFile).catch(console.log) // ❌ 报错,异常逃逸

这里,Promise.resolve() 虽然能将回调的返回值封装为 promise 实例,但它无法捕获回调内部的同步异常。所以,同步异常会逃逸,最终导致程序执行终端并报错。

同理,采用 ES2024 新增的 Promise.withResolvers() 方法,也会导致异常逃逸:

js 复制代码
Promise.try = function promiseTry(fn, ...args) {
  let { promise, resolve } = Promise.withResolvers()
  resolve(fn(...args))
  return promise
}

Promise.try(readLocalFile, './package.json').then(console.log) // ✅
Promise.try(readLocalFile).catch(console.log) // ❌ 报错,异常逃逸

如果你非要用上述两种 API 来模拟实现 Promise.try(),那只能手动 try/catch 处理同步异常,并转化为失败的 promise。

以 ES2024 的 Promise.withResolvers() 为例:

js 复制代码
Promise.try = function promiseTry(fn, ...args) {
  let { promise, resolve, reject } = Promise.withResolvers()
  try {
    resolve(fn(...args))
  } catch (e) {
    reject(e)
  }
  return promise
}

这种方案允许我们捕获同步异常,并转化为失败的 promise,但混用了同/异步的异常处理方式,比超人内裤外穿还碍眼。

丢失同步行为

为了利用 Promise 自动捕获同步异常的机制,有人采用了 then() 方法来包裹:

js 复制代码
Promise.try = function promiseTry(fn, ...args) {
  return Promise.resolve().then(() => fn(...args))
}

Promise.try(readLocalFile).catch(console.log) // ❌ path 不能为空
Promise.try(readLocalFile, './package.json').then(console.log) // ✅

这里,我们利用了 then() 方法的底层机制,其内部会自动捕获异常,并转化为失败的 promise,不需要我们手动 try/catch

可以看到,在这种场景下,我们得到了和原生 Promise.try() 一致的结果。bug 在于,fn() 函数不要求一定是异步函数,它可能是一个同步执行的回调,但我们将其放在 then() 方法中,它被强制转化为一个永远只能异步执行的微任务。

热补丁

不同于 then()new Promise()executor() 是同步调用的 阻塞型回调

javascript 复制代码
console.log('sync:', 1)

function maybeSync() {
  console.log('maybeSync:', 2)
  throw new Error('同步异常')
}

new Promise(function executor(resolve) {
  resolve(maybeSync())
})
  .then(() => {
    console.log('async:', 3)
  })
  .catch((e) => {
    console.log(`catch ${e}:`, 4)
  })

console.log('sync:', 5)
/**
 * sync: 1
 * maybeSync: 2
 * sync: 5
 * catch 同步异常: 4
 */

因此,ES2025 之前,采用 new Promise() 模拟 Promise.try() 是一种可行的 热补丁

js 复制代码
Promise.try = function promiseTry(fn, ...args) {
  return new Promise((resolve, reject) => {
    try {
      resolve(fn(...args))
    } catch (e) {
      reject(e)
    }
  })
}

这里,new Promise() 内部调用回调,同时将返回值封装为一个 promise 实例。

由于 ES6 标准的 Promise 构造函数内部会自动捕获异常,并转化为失败的 promise 实例,所以上述代码可以优化为:

js 复制代码
Promise.try = function (f, ...args) {
  return new Promise((resolve) => {
    resolve(f(...args))
  })
}

两种实现方式的功能是等价的,只是后者更加精简。

此外,async function 也能模拟 Promise.try()

javascript 复制代码
Promise.try = async function (f, ...args) {
  return f(...args)
}

那为何还需要 Promise.try()

async function 初学者会误解其内代码都异步执行,其实没有 awaitasync function 会同步执行。同理,它们会误解 async function 始终返回成功的 promise,除非函数体中存在 try/catch

Promise.try() 更直观,能减少初学者的认知负荷。TC39 委员如是说,"Promise API 和 async/await 语法应互补实现等价功能,Promise.try() 是缺失的拼图。async/await 语法无法取代 Promise API,让它们并行不悖至关重要。"

高潮总结

根据《ecmascript 语言规范》,ES2025 新增 promise.try() 静态方法,用于调用可能返回 promise 的回调,最终返回 promise。

实际开发中,部分用户为了不安装第三方模块,会手动模拟实现 Promise.try() 方法,在不兼容的平台中使用这种现代 API。然而,部分实现采用了 Promise.resolve()then() 方法错误实现,会不小心引入 bug。

推荐采用原生 Promise.try(),或集成 polyfill 扩展来重构代码屎山,消除技术负债。如果要手写 Promise.try(),请使用 new Promise() 的方案。

相关推荐
2501_920931701 小时前
React Native鸿蒙跨平台实现推箱子游戏,完成玩家移动与箱子推动,当所有箱子都被推到目标位置时,玩家获胜
javascript·react native·react.js·游戏·ecmascript·harmonyos
layman05282 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔2 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李2 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN2 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒2 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局
PHP武器库2 小时前
ULUI:不止于按钮和菜单,一个专注于“业务组件”的纯 CSS 框架
前端·css
方也_arkling2 小时前
Element Plus主题色定制
javascript·sass
电商API_180079052472 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
晓晓莺歌2 小时前
vue3某一个路由切换,导致所有路由页面均变成空白页
前端·vue.js