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