如何错误手写 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 初学者会误解其内代码都异步执行,其实没有 await 的 async 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() 的方案。

相关推荐
云浪8 小时前
Go 并发实战:从零实现一个高并发安全的任务调度器
javascript·后端·go
Flynt9 小时前
axios 被投毒半年后,我把 npm 的"安全防线"实测了一遍:最信任的 SHA512 是安慰剂
前端·npm·node.js
excel9 小时前
Base62 和 Base64 有什么区别?
前端
风骏时光牛马11 小时前
大模型底层架构与AI源码深度剖析
前端
掘金挖土12 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
中文呀14 小时前
三维 GIS 离线缓存实战:逆向 CesiumOfflineCache,并亲手为它补上"自动更新"能力
前端
前进前进前进15 小时前
TRAE 全链路实战:做了一个警示牌一键生成器:纯 Canvas、单文件、零依赖
前端
小万74216 小时前
God‘s Eye View 深度解析:15 路公开信号 + CesiumJS 的浏览器级 OSINT 控制台,60 帧渲染的“真实间谍卫星模拟器”
前端
IT星宿16 小时前
浏览器标签页全是一个图标:我用名字哈希生成 favicon,让每个项目都有身份标识
前端