全文速览
欢迎关注 前端情报社。大家好,我是社长林语冰。
Promise
从 ES2015 成为 JavaScript 的一部分。10 年后,ES2025 是第 16 版 JavaScript 语言规范,它新增了 9 种颠覆性功能,Promise.try()
就是其中之一。
顾名思义,Promise.try()
为 Promise
类新增了一个静态方法,它接收一个 行为不可知的阻塞型回调:
- 它可能是异步函数;
- 它可能返回 promise;
- 它可能引发异常
- .....
然后 立即调用 该回调,最终返回一个 promise。
本文我们会探讨 ES2025 最新 Promise.try()
静态方法的基本用法,高级用例,底层原理和编程技巧。
ES2025 Promise.try()
Promise.try()
提案并非原创,ES2025 之前,bluebird 和 p-try 等流行库就提供了等价的功能。
bluebird 官方文档提供了基本示例:
js
function getUserById(id) {
return Promise.try(function () {
if (typeof id !== 'number') {
throw new Error('id 要求为数字!')
}
return db.getUserById(id)
})
}
getUserById().catch(console.log)
// Error: id 要求为数字!
现实开发中的代码往往错综复杂,有的业务逻辑可能混用同步/异步操作。上述代码中,输入验证是同步错误,数据库操作可能是异步操作。Promise.try()
可以用于封装这些复杂业务,确保无论回调是否异步执行或报错,都能返回一个 promise,继续链式调用。
这就是 ES2025 Promise.try()
的用途,但我们不需要再安装 bluebird 等第三方库。
具体而言,Promise.try()
接受一个行为不可知的 阻塞型回调 并立即调用它,最终返回 promise:
javascript
// ES2025 之后的写法:
// ✅️ 1. 回调返回非 promise
Promise.try(() => '同步结果').then(console.log)
// ✅️ 2. 回调同步报错
Promise.try(() => {
throw new Error('同步异常')
}).catch(console.log)
// ✅️ 3. 回调返回成功的 promise
Promise.try(() => Promise.resolve('fulfillment')).then(console.log)
// ✅️ 4. 回调返回失败的 promise
Promise.try(() => Promise.reject('rejection')).catch(console.log)
// ✅️ 5. 回调是正常执行的异步函数
Promise.try(async () => {
let data = await Promise.resolve('异步结果')
// 其他业务......
return data
}).then(console.log)
// ✅️ 6. 回调是异步报错的异步函数
Promise.try(async () => {
try {
let result = await Promise.reject('异步异常')
} catch (e) {
throw e
}
}).catch(console.log)
可以看到,Promise.try()
是一个更加强大和通用的现代原生 API,适用于各种复杂的回调场景。
另请参考,其 TypeScript 源码的函数签名如下:
ts
interface PromiseConstructor {
try<T, U extends unknown[]>(
callbackFn: (...args: U) => T | PromiseLike<T>,
...args: U
): Promise<Awaited<T>>
}
底层原理
ES2025 之前,想要实现 Promise.try()
的等价功能,除了引入第三方模块,还可以使用 new Promise()
手动封装,只要你懂得基本的底层原理。
具体而言,new Promise()
模拟 Promise.try()
的底层原理如下:
js
Promise.try = function (f, ...args) {
return new Promise((resolve) => {
resolve(f(...args))
})
}
这里,new Promise()
内部调用回调,同时将返回值封装为一个 promise 实例。
比起安装第三方模块或手动封装,ES2025 原生的 Promise.try()
显然更符合人体工程学。
不同于 new Promise(resolve => resolve(f()))
这种遗臭万年的代码屎山,Promise.try(f)
是一种更精简的"代码高尔夫":你能用 更少的字符 重构等价的功能。
薛定谔的异步
现实开发中,某些 API 的回调可能同步/异步执行:
js
let map = new Map([[1, 'cache']])
function log(data) {
console.log(`callback: ${data}`)
}
function zalgoAPI(id, cb) {
if (map.has(id)) {
// 若缓存命中,则回调同步执行
cb(map.get(id))
} else {
// 若缓存未命中,则回调异步执行
setTimeout(() => {
map.set(id, 'update data')
cb(map.get(id))
}, 1_000)
}
}
console.log('sync:', 1)
zalgoAPI(1, log)
zalgoAPI(2, log)
console.log('sync:', 2)
/**
* sync: 1
* callback: cache
* sync: 2
* callback: update data
*/
"npm 之父"将这种难以预测的设计屎山称为 Zalgo 问题(混沌问题)。
为了解决 Zalgo 问题,我们可以使用 Promise.try()
简单重构,确保回调始终异步执行:
js
import { setTimeout as setTimeoutPromise } from 'node:timers/promises'
let map = new Map([[1, 'async cache']])
function asyncAPI(id) {
return Promise.try(() => {
if (map.has(id)) {
return map.get(id)
} else {
return setTimeoutPromise(1_000).then(() => {
map.set(id, 'async data')
return map.get(id)
})
}
})
}
console.log('sync:', 1)
asyncAPI(1).then(log)
asyncAPI(2).then(log)
console.log('sync:', 2)
/**
* sync: 1
* sync: 2
* callback: cache
* callback: update data
*/
实用技巧
此外,类似 setTimeout()
,Promise.try()
支持 实参转发:
javascript
setTimeout(function closure() {
console.log('ES2025')
}, 1_000)
// 👇️ 实参转发
setTimeout(console.log, 1_000, 'ES2025')
// ********************************
Promise.try(function closure() {
console.log('ES2025')
})
// 👇️ 实参转发
Promise.try(console.log, 'ES2025')
两种写法功能等价,但后者减少了冗余闭包,性能更棒。
浏览器兼容性
2025 年 1 月,Promise.try()
成为 Baseline 基准可用 新特性,所有最新主流浏览器都原生支持。
在尚不支持 Promise.try()
的旧平台中,可以按需引入 polyfill(功能补丁) 优雅降级。
以 GitHub 人气最高的 core-js
为例,先用 npm / pnpm 安装 core-js
模块:
bash
npm install core-js@latest
# 或者:
pnpm install core-js@latest
然后导入开箱即用的 polyfill,更多细节请参考 core-js
官方文档:
javascript
// 集成 polyfill
import 'core-js/es/promise/try.js'
// 基本用法
Promise.try(console.log, 'Hello ES2025')
高潮总结
根据《ECMAScript 语言规范》,es2025 新增 Promise.try()
静态方法,用于调用可能返回 promise 的回调,最终返回 promise。
作为一个更通用的现代 API 糖 ,Promise.try()
无差别执行开发者提供的回调,高效且稳健地开启 Promise
链,更符合人体工程学。
有了 Promise.try()
,库作者免于反复造轮子手写样板代码,也避免了错误模拟 Promise.try()
引入潜在 bug,更避免了集成 bluebird 等第三方库增加打包体积。
推荐采用原生 Promise.try()
,或集成 polyfill 扩展来重构代码屎山,消除技术负债。