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 语言风格的错误处理模式, 把结果包裹为一个元组. 优势:
- 没有 try...catch, 函数整体层级变得扁平
- 错误优先处理. 首先通过一个 if 语句检查并处理错误 (这被称为卫语句或 GuardClause), 然后提前返回. 不会漏处理错误
- 可读性高: 处理完错误后剩下的代码都是成功路径下的核心逻辑. 一目了然, 不会再有嵌套
更安全的错误处理
实战中, 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)

浙公网安备 33010602011771号