使用 Promise.try() 优雅地同时处理同步与异步错误

Published
Updated
CategoryDev recipes
Available in

TL;DR: 其实就是能少写一个 try-catch 块。

在 JavaScript 异步编程的演进历程中,Promise 一直是核心角色。然而,当回调函数可能同步抛出错误也可能异步返回 Promise 时,开发者往往需要编写繁琐的错误处理代码。Promise.try() 的引入正是为了优雅地解决这个问题——它统一了同步与异步错误处理的边界,让我们能够用一致的方式处理所有可能的函数执行结果。

什么是 Promise.try()

Promise.try() 是 JavaScript 中的一个静态方法,它接收任意类型的回调函数(无论是同步还是异步、返回值或抛出错误),并将其结果统一包装为一个 Promise。这意味着无论函数内部是立即返回一个值、抛出异常,还是返回一个异步的 Promise,Promise.try() 都会确保你得到的是一个可以链式调用的 Promise 对象。

语法非常直观:

Promise.try(func)
Promise.try(func, arg1, arg2, ...etc)

其中 func 是你想要调用的函数,后续的 arg1arg2 等是传递给该函数的参数。

为什么需要 Promise.try()

在日常开发中,我们经常面临一个棘手的问题:某些回调函数可能是同步执行的,也可能是异步执行的,而我们需要对它们的结果进行统一处理。考虑以下场景:

// 尝试使用 Promise.resolve 包装函数调用
function runCallback(callback) {
  return Promise.resolve(callback())
}

这种写法看起来简洁,但存在一个致命缺陷:如果 callback() 同步抛出了错误,这个错误不会被捕获并转换成 Promise 的拒绝状态,而是会直接抛出到外部,导致程序崩溃。

传统上,开发者需要这样处理:

function runCallbackSafely(callback) {
  try {
    const result = callback()
    return Promise.resolve(result)
  } catch (error) {
    return Promise.reject(error)
  }
}

这种模式虽然能工作,但代码冗长且需要手动编写 try-catch 逻辑。Promise.try() 的出现将这一切简化为单行代码。

核心用法与示例

基础用法

Promise.try() 最基本的用法就是将一个函数的调用“提升”为 Promise:

function doSomething() {
  return 'Success!'
}

Promise.try(doSomething)
  .then(result => console.log(result)) // "Success!"
  .catch(error => console.error(error))

处理同步错误

当函数同步抛出错误时,Promise.try() 会自动将其捕获并转换为 Promise 的拒绝状态:

function mightFail() {
  throw new Error('Something went wrong!')
}

Promise.try(mightFail)
  .then(result => console.log(result))
  .catch(error => console.error(error.message)) // "Something went wrong!"

处理异步函数

对于返回 Promise 的异步函数,Promise.try() 同样适用:

async function fetchData() {
  const response = await fetch('/api/data')
  return response.json()
}

Promise.try(fetchData)
  .then(data => console.log(data))
  .catch(error => console.error('Fetch failed:', error))

传递参数

Promise.try() 支持直接传递参数给回调函数,无需创建额外的闭包:

function greet(name, greeting) {
  return `${greeting}, ${name}!`
}

// 传统方式:创建闭包
Promise.resolve().then(() => greet('World', 'Hello'))

// Promise.try 方式:直接传递参数
Promise.try(greet, 'World', 'Hello')
  .then(message => console.log(message)) // "Hello, World!"

这种方式不仅代码更简洁,还避免了创建不必要的闭包,性能更优。

实际应用场景

场景一:统一 API 错误处理

假设你正在构建一个库,允许用户传入回调函数。这些回调可能是同步的也可能是异步的,你希望以统一的方式处理它们的结果:

class TaskRunner {
  execute(task, ...args) {
    return Promise.try(task, ...args)
      .then(result => ({
        success: true,
        data: result,
      }))
      .catch(error => ({
        success: false,
        error: error.message,
      }))
  }
}

const runner = new TaskRunner()

// 同步任务
runner.execute(() => 42)
  .then(console.log) // { success: true, data: 42 }

// 可能抛出错误的任务
runner.execute(() => { throw new Error('Oops') })
  .then(console.log) // { success: false, error: "Oops" }

// 异步任务
runner.execute(async () => {
  await new Promise(resolve => setTimeout(resolve, 100))
  return 'Done!'
}).then(console.log) // { success: true, data: "Done!" }

场景二:类同步风格的错误处理

Promise.try() 配合 catch()finally() 可以实现类似同步代码的错误处理风格:

function processUserData(userData) {
  return Promise.try(() => validateUser(userData))
    .then(user => enrichUserData(user))
    .then(enriched => saveToDatabase(enriched))
    .catch((error) => {
      console.error('Processing failed:', error)
      throw error // 重新抛出或返回默认值
    })
    .finally(() => {
      console.log('Processing attempt completed')
      cleanupResources()
    })
}

这种链式调用让异步错误处理变得清晰直观,避免了嵌套的 try-catch 块。

场景三:第三方库集成

当你集成第三方库时,某些函数可能会在某些情况下同步抛出错误,在另一些情况下返回 Promise。Promise.try() 让你无需关心这些细节:

function callThirdPartyAPI(config) {
  return Promise.try(() => thirdPartyLibrary.process(config))
    .then(result => normalizeResponse(result))
    .catch((error) => {
      metrics.recordError('third_party_error', error)
      return fallbackResponse
    })
}

场景四:函数式编程中的组合

在函数式编程风格中,Promise.try() 可以作为函数组合的基础构建块:

function pipePromise(value, ...fns) {
  return fns.reduce((acc, fn) => acc.then(result => Promise.try(fn, result)), Promise.resolve(value))
}

const addOne = x => x + 1
const asyncDouble = async x => x * 2
function mightThrow(x) {
  if (x > 10) throw new Error('Too big!')
  return x
}

pipePromise(5, addOne, asyncDouble, mightThrow)
  .then(console.log) // 12
  .catch(console.error)

与 async/await 的对比

Promise.try() 可以用 async/await 实现类似的逻辑:

// Promise.try 版本
Promise.try(() => riskyOperation())
  .then(handleSuccess)
  .catch(handleError);

// async/await 等价版本
(async () => {
  try {
    const result = await riskyOperation()
    return handleSuccess(result)
  } catch (error) {
    return handleError(error)
  }
})()

在简单场景下两者可以互换,但 Promise.try() 在某些情况下更加简洁:

  1. 当你只需要包装单个函数调用时
  2. 当你需要在 Promise 链中插入同步操作时
  3. 当你希望避免立即执行函数表达式(IIFE)的语法噪音时

浏览器兼容性与 Polyfill

Promise.try() 是 ES2024(ES15)引入的标准特性。在获得原生支持之前,可以使用以下 polyfill:

if (!Promise.try) {
  Promise.try = function (callback, ...args) {
    return new Promise((resolve) => {
      resolve(callback.apply(this, args))
    })
  }
}

总结

Promise.try() 是 JavaScript Promise API 的一个重要补充,它解决了长期以来同步与异步错误处理不一致的问题。通过将任意函数的执行结果统一提升为 Promise,它让我们能够:

  1. 简化错误处理逻辑:不再需要手动编写 try-catch 来捕获同步错误
  2. 统一接口设计:无论回调是同步还是异步,都能以相同方式处理
  3. 提高代码可读性:链式调用让异步流程更加清晰
  4. 优化性能:直接传递参数避免不必要的闭包创建

在需要处理可能抛出同步错误的异步操作时,Promise.try() 是一个优雅且实用的选择。它不仅减少了样板代码,更重要的是让 JavaScript 的错误处理模型更加一致和可预测。