普通视图

发现新文章,点击刷新页面。
昨天以前首页

如何错误手写 ES2025 新增的 Promise.try() 静态方法

2025年9月4日 22:48

全文速览

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

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 风格,然后安全开启链式调用的场景:

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() 的行为,其实是一种错误的方案:

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() 方法,也会导致异常逃逸:

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() 为例:

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() 方法来包裹:

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() 是同步调用的 阻塞型回调

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() 是一种可行的 热补丁

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 实例,所以上述代码可以优化为:

Promise.try = function (f, ...args) {
  return new Promise((resolve) => {
    resolve(f(...args))
  })
}

两种实现方式的功能是等价的,只是后者更加精简。

此外,async function 也能模拟 Promise.try()

Promise.try = async function (f, ...args) {
  return f(...args)
}

那为何还需要 Promise.try()

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

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

2025年9月4日 22:46

全文速览

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

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

❌
❌