如何错误手写 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() 的方案。

相关推荐
葡萄城技术团队2 分钟前
Excel 转在线协作难题破解:SpreadJS 纯前端表格控件的技术方案与实践
前端·excel
我的xiaodoujiao2 分钟前
Windows系统Web UI自动化测试学习系列3--浏览器驱动下载使用
前端·windows·测试工具·ui
一只小风华~5 分钟前
学习笔记:Vue Router 中的嵌套路由详解[特殊字符]概述
前端·javascript·vue.js
泻水置平地5 分钟前
若依前后端分离版实现前端国际化步骤
前端
Villiam_AY7 分钟前
从后端到react框架
前端·react.js·前端框架
CodeCraft Studio10 分钟前
全球知名的Java Web开发平台Vaadin上线慧都网
java·开发语言·前端·vaadin·java开发框架·java全栈开发·java ui 框架
一只小风华~17 分钟前
Vue Router 命名路由学习笔记
前端·javascript·vue.js·笔记·学习·ecmascript
我是华为OD~HR~栗栗呀21 分钟前
前端面经-高级开发(华为od)
java·前端·后端·python·华为od·华为·面试
超级大只老咪1 小时前
HTML学习路线
前端·学习·html
゜ eVer ㄨ1 小时前
React学习第三天——生命周期
前端·学习·react.js