Promise 再次进化,ES2025 新增 Promise.try() 静态方法

712 阅读4分钟

全文速览

欢迎关注 前端情报社。大家好,我是社长林语冰。

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 官方文档提供了基本示例:

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:

// 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 源码的函数签名如下:

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() 的底层原理如下:

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 的回调可能同步/异步执行:

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() 简单重构,确保回调始终异步执行:

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() 支持 实参转发

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 模块:

npm install core-js@latest
# 或者:
pnpm install core-js@latest

然后导入开箱即用的 polyfill,更多细节请参考 core-js 官方文档:

// 集成 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 扩展来重构代码屎山,消除技术负债。