JS 替代 try catch 的更优雅的错误处理模式-兼容同步出错版本

看其他人的博客时, 发现了类 Go 的错误处理方式很精妙, 笔者在其上还加了一些改进.

问题

我们需要在 try catch 中处理异步函数的错误, 代码如下

try {
  const result = await fetch('someAPI')
  console.log('fetch success and get', result)
  try {
    await doSomeThingAsync(result) 
  } catch (err) {
    console.error('doSomeThingAsync error', err)
  }
} catch (error) {
  console.error('fetch error', error)
}

这种方式有一个问题, 就是 try catch 层级多了一层, 而且随着业务逻辑变的复杂, 可能会嵌套起来.

如果不想嵌套, 写法可能改成这样

let result
try {
  result = await fetch('someAPI')
  console.log('fetch success and get', result)
} catch (error) {
  console.error('fetch error', error)
}

try {
  await doSomeThingAsync(result)
} catch (err) {
  console.error('doSomeThingAsync error', err)
}

如果你在 TS 写代码会经常头疼于处理 result 没有预先定义类型, doSomeThingAsync 方法里不认它是合法入参.

那么问题来了, 有没有一种可能即可以让代码变得清爽不用嵌套, 又不用花力气做额外的 TS 定义.

实践片段

参考 Go 语言和 Rust 语言风格的错误处理方式

const [error, result] = await to(fetch('someAPI'))
if (error) {
  console.error('fetch error', error)
  return
}

const [err] = await to(doSomeThingAsync(result))
if (err) {
  console.error('doSomeThingAsync error', err)
}

引入一个方法包裹原先的执行 API 动作, 并把错误和数据都给返回. 再优先处理错误, 使得剩下的语句一定是成功路径. 可以解开嵌套, 免写定义.

函数代码

/**
 * 接收一个 Promise, 并返回一个元组 [error, data]
 *
 * @param {Promise<T>} promise - 待处理的 Promise 对象
 * @returns {Promise<[Error | null, T | undefined]>} - 返回一个 Promise 对象,该对象解析为一个包含错误和数据的元组
 * @example
 * ```
 * const [error, text] = await to(fetch('https://baidu.com'))
 * if (error || !text) {
 *   // todo 处理失败逻辑
 *   console.error('fetch error', error)
 *   return
 * }
 * console.log('fetch success and get', text)
 * ```
 */
export function to<T>(promise: Promise<T>): Promise<[Error | null, T | undefined]> {
  try {
    return promise
      .then<[null, T]>((data: T) => [null, data])
      .catch<[Error, undefined]>((error: Error) => [error, undefined]);
  } catch (err) {
    const error = err as Error
    return Promise.resolve([error, undefined])
  }
  
}

核心思想就是用 Go, Rust 语言风格的错误处理模式, 把结果包裹为一个元组. 优势:

  1. 没有 try...catch, 函数整体层级变得扁平
  2. 错误优先处理. 首先通过一个 if 语句检查并处理错误 (这被称为卫语句或 GuardClause), 然后提前返回. 不会漏处理错误
  3. 可读性高: 处理完错误后剩下的代码都是成功路径下的核心逻辑. 一目了然, 不会再有嵌套

更安全的错误处理

实战中, API 内部可能有其他同步形式抛错, 如入参校验, 看个例子:

// 示例 API 代码, 把入参加 1 然后包装 Promise 返回.
function myAPI(num: number): Promise<number> {
  // 入参校验
  if (typepf text !== 'number') {
    throw new Error('number is required')
  }
  return Promise.resolve(num + 1)
}

如果用上文的 to 函数, 如果这样处理仍然不够. 因为上文已经抛错 'number is required'. 这是同步抛出的错误, 我们的 to 只能处理异步的 Promise

const [err, result] = await to(myAPI('123'))
// 注意: !!! 走不入下面的代码, 'number is required' 错误已经逃到 to 函数外层, 抛给上层函数了.
if (err) {
  console.error('myAPI error', err)
  return
}
console.log('myAPI success' result)

那么为了防止 API 是同步的, 或者入参校验会同步抛出错误来, 我们进一步包装为

/**
 * 安全执行函数:接收一个函数(可能返回 Promise 或同步值),统一返回 Promise 包裹的元组 [error, data]
 * 能够捕获同步抛出的错误和异步 Promise reject
 *
 * @param {() => T | Promise<T>} fn - 要执行的函数,可能返回同步值或 Promise
 * @returns {Promise<[Error | null, T | undefined]>} - 返回一个 Promise 对象,该对象解析为一个包含错误和数据的元组
 * @example
 * ```
 * // 处理可能同步抛错的 API 调用
 * const [error1] = await toSafe(() => window.someMethod(params))
 * if (error1) {
 *   console.error('API call error', error1)
 *   return
 * }
 *
 * // 处理异步 Promise
 * const [error2, data2] = await toSafe(() => fetch('https://api.example.com'))
 * if (error2) {
 *   console.error('fetch error', error2)
 *   return
 * }
 *
 * // 处理同步函数, 彼此不用嵌套和干扰.
 * const [error3, data3] = await toSafe(() => JSON.parse(jsonString))
 * ```
 */
export function toSafe<T>(fn: () => T | Promise<T>): Promise<[Error | null, T | undefined]> {
  try {
    // 尝试执行函数
    const result = fn();

    // 判断结果是否为 Promise
    if (
      result &&
      typeof result === 'object' &&
      'then' in result &&
      typeof result.then === 'function'
    ) {
      // 处理 Promise 情况
      return (result as Promise<T>)
        .then<[null, T]>((data: T) => [null, data])
        .catch<[Error, undefined]>((error: Error) => [error, undefined]);
    } else {
      // 处理同步返回值
      return Promise.resolve([null, result] as [null, T]);
    }
  } catch (error) {
    // 捕获同步抛出的错误
    const err = error instanceof Error ? error : new Error(String(error));
    return Promise.resolve([err, undefined] as [Error, undefined]);
  }
}

那么调用方式改写为

const [err, result] = await toSafe(() => myAPI('123'))
if (err) {
  console.error('myAPI error', err)
  return
}
console.log('myAPI success' result)
posted @ 2025-06-30 10:25  Ever-Lose  阅读(31)  评论(0)    收藏  举报