Published
- 6 min read
TypeScript实现优雅错误处理
前言
众所周知,JavaScript采用 try {} catch {}
语句捕获错误,然而,在需要细粒度处理错误的场景下,try {} catch {}
语句会使代码结构变得异常臃肿。
考虑如下场景,在一个包含多个流程调用处理函数中,我们需要捕获函数的运行错误,并针对不同流程的失败进行不同的处理,如果使用 try {} catch {}
语句进行错误捕获,可能的代码实现如下:
async function writeFile(path: string, content: string) {
let fd: FileHandle
try {
fd = await fs.promises.open(path, 'w')
} catch {
throw new Error(`open file failed`)
}
try {
await fd.write(content)
} catch {
throw new Error(`write file failed`)
} finally {
await fd.close()
}
}
以上代码中,每处理一个错误场景都需要由一个 try {} catch {}
语句包裹,代码结构极为臃肿。
Errors are values
熟悉Go语言的朋友都知道,Go语言对错误处理的设计理念是「Errors are values」,即把错误当成普通值对待,错误与其他值地位相等,并没有特殊之处。
因此,Go语言中并没有其他编程语言中常见的 try {} catch {}
语句,而是通过一个通俗的约定实现错误处理:函数支持返回多个值,错误作为最后一个值返回,调用方通过条件判断语句进行错误处理。
笔者认为,Go语言的错误处理方式是细粒度的错误处理,这种方式强制开发者对每一个函数调用的错误进行处理(这也是Go语言错误处理被众多开发者诟病的一个原因),与之相反,传统的 try {} catch {}
语句则是粗粒度的处理方式,开发者只需把代码逻辑包裹在 try {}
语句中,即可在一个 catch {}
块中对错误进行统一处理。
针对上述的例子,我们所需要的恰恰就是细粒度的错误处理,如果用Go语言的错误处理方式重构上述代码案例,可能的代码实现如下:
async function writeFile(path: string, content: string) {
const [fd, openFailError] = await openFileHandle(path, 'w')
if (openFailError) {
throw new Error(`open file failed`)
}
const [_, writeFailError] = await writeFile(fd, content)
await closeFileHandle(fd)
if (writeFailError) {
throw new Error(`write file failed`)
}
}
重构后的代码由14行减少为10行(不计空行),没有了臃肿的 try {} catch {}
语句,代码更加清爽。
包装器
针对上述需求,可以包装一个高阶函数,用于捕获函数运行错误并以类似Go语言多返回值的形式返回元组,预期实现目标:
- 支持同步/异步函数包装
- 类型推断友好
代码实现
export type Promisify<T> = T extends Promise<any> ? T : Promise<T>
export type UnPromisify<T> = T extends Promise<infer U> ? U : T
export type CatchItResult<T extends (...args: unknown[]) => unknown, E> =
| [UnPromisify<ReturnType<T>>, null]
| [null, NonNullable<E>]
export type CatchIt<T extends (...args: unknown[]) => unknown, E = never> = (
...args: Parameters<T>
) => ReturnType<T> extends Promise<any> ? Promise<CatchItResult<T, E>> : CatchItResult<T, E>
const isPromise = <T>(o: unknown): o is Promise<T> => {
return o instanceof Promise
}
export const catchIt = <T extends (...args: any[]) => any, E = Error>(fn: T) => {
return ((...args: Parameters<T>) => {
try {
const result = fn(...args)
return isPromise<UnPromisify<ReturnType<T>>>(result)
? result.then((result) => [result, null]).catch((e) => [null, e])
: [result, null]
} catch (e) {
return [null, e]
}
}) as CatchIt<T, E>
}
使用方式
catchIt(fn)
返回包装过的 fn()
函数,调用包装过的函数可以使用条件判断的形式处理错误。
const f1 = catchIt((a: string, b: number) => b)
// 同步函数调用,类型推导正确
// 参数类型为[string, number],推导正确
const [r1, e1] = f1('', 0)
if (e1) {
// 捕获到错误时,返回值为null,类型正确
const r: null = r1
} else {
// 未捕获到错误时,返回值为number,类型正确
const r: number = r1
}
const f2 = catchIt(() => '')
// 无参数调用,类型推导正确
const [r2, e2] = f2()
if (e2) {
const r: null = r2
} else {
const r: string = r2
}
const f3 = catchIt(async (a: boolean) => a)
// 异步函数调用,类型推导正确
const [r3, e3] = await f3(true)
if (e3) {
const r: null = r3
} else {
const r: boolean = r3
}
结语
本文介绍了在需要细粒度进行错误处理场景下的一种解决方案,引入了一个包装器函数用于实现仿Go语言的错误处理机制。
笔者无意引起语言优劣之争,2种错误处理方式各有适用的场景,可以相互结合使用。