全文速览
欢迎关注 前端情报社。大家好,我是社长林语冰。
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() 的方案。