背景
下面这段代码有何改进之处?
export function foo(value: string): string {
if (!isNumber(value)) {
return '';
}
return 'xxx'
}
答:错误不能直接返回 '' 会和正常返回值混淆,导致调用者没法做异常处理。
基于以上背景,本文将总结几种错误处理方式,供大家参考。
1 throw error
- 缺点:如果忘记错误处理会导致运行时报错,需要在函数描述增加显著的
@throws告知必须错误处理; - 优点:天然的符合直觉的错误处理方式,可和正常返回值区分,开发者能针对性做错误处理。
- 适用场景:多种类型的错误。
建议结合 box 一起使用。
/**
* **注意:参数非法会报错,请自行 try-catch **
*
* @throws RangeError 显著告示报错将返回 RangeError
* @returns {string}
*/
function foo(arg1: string, arg2: string) | string {
if (!/^\d+$/.test(arg1)) {
throw new RangeError(`arg1 (${arg1}) 只能是整数`)
}
const ranges = ['bar', 'baz'];
if (!ranges.includes(arg2)) {
throw new RangeError(`arg2 (${arg2}) 只能是 ${ranges.join(', ')} 其一`)
}
...
return 'result';
}
let result;
try {
// 建议只是 try catch 局部,否则大而全的 catch 违反了代码规约:
// > 对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
result = foo('hello', 'world')
} catch {
return rc.error('foo 报错', { error: result })
}
// 正常处理
result
2 返回 null + console.error 描述(可选)
- 优点:不会忘记错误处理而导致页面崩溃,最坏情况是页面展示
null; - 缺点:调用者无法捕捉到精确的错误类型做二次错误处理,比如上报监控等。
- 建议使用场景:调用者无需。
export function getBankLogoURL(instId: string, type: ILogoType, ...): null | string {
if (!instId || !type) return null;
...
return `${PATH}/${subpath}`;
}
const url = getBankLogoURL(...args);
// 如果有多种错误只能笼统上报 `'获取 logo 失败'` 😅
if (!url) { return rc.error('获取 logo 失败', { args }) }
// 这里 url 必定是字符串
url.toLowerCase()
3 返回 error
- 优点:开发者能精确捕获错误信息上报各种类型的错误,错误不会被忘记处理,否则 ts 类型会报错。
- 缺点:极端情况下如果函数正常返回值也是 Error 类型,那就无法区分了。
/**
* @throws no error
* @returns {Error | string} 显著告示报错将返回 Error
*/
function foo(arg1: string, arg2: string): Error | string {
if (!/^\d+$/.test(arg1)) {
return new TypeError(`arg1 (${arg1}) 只能是整数`)
}
const ranges = ['bar', 'baz'];
if (!ranges.includes(arg2)) {
return new RangeError(`arg2 (${arg2}) 只能是 ${ranges.join(', ')} 其一`)
}
...
return 'result';
}
declare var rc: { error: any }
const result = foo('hello', 'world')
// 逼迫必须进行异常处理,否则无法使用
// ❌ TS: Property 'toLowerCase' does not exist on type 'RangeError'.(2339)
result.toLowerCase()
if (result instanceof Error) {
// 能上报完整的错误信息
return rc.error({ error: result })
}
// 可正常使用
const lowerCased = result.toLowerCase();
4 返回二元祖
go 语言采用,当然也是被吐槽较多的,因为会出现错误处理模板,但是被 go 的开发者回复过。
第一项是错误,第二项是结果。const [error, result] = doSth(...)。
- 优点:兼顾二者,同时逼迫开发者必须错误处理;
- 缺点:写法较为冗余,违背前端开发者的习惯。
/**
* @throws no error
* @returns 返回二元组,第一项是错误,第二项是结果
*/
function foo(arg1: string, arg2: string): [Error | undefined, string | undefined] {
if (!/^\d+$/.test(arg1)) {
return [new TypeError(`arg1 (${arg1}) 只能是整数`)]
}
const ranges = ['bar', 'baz'];
if (!ranges.includes(arg2)) {
return [new RangeError(`arg2 (${arg2}) 只能是 ${ranges.join(', ')} 其一`)]
}
...
return [undefined, 'result'];
}
declare var rc: { error: any }
const [error, result] = foo('hello', 'world')
// 逼迫必须进行异常处理,否则无法使用
if (error) {
// 能上报完整的错误信息
return rc.error({ error: result })
}
// 正常逻辑处理
if (!result) {
const lowerCased = result.toLowerCase();
}
5 参数控制是否抛错 - 混合派
比如通过参数控制是否选择抛出异常还是返回 null。社区案例:currency.js
- 优点:灵活的设计默认安全、默认简洁、也能做详细针对性的错误处理。
- 缺点:多一个参数
/**
*
* @returns throwsException false 则报错返回 null,否则 throw Error
* @throws TypeError 当非整数传入且 throwsException 设置 true 则抛错
*/
function foo(
arg1: string,
arg2: string,
{ throwsException = false } = {} // 默认不抛错
): null | string {
if (!/^\d+$/.test(arg1)) {
if (throwsException) { throws new TypeError(`arg1 (${arg1}) 只能是整数`) }
return null
}
...
return 'result';
}
const result = foo(arg1, arg2);
if (result !== null) { setState(result) }
let result: string;
try {
result = foo(arg1, arg2, { throwsException = true });
} catch (error) {
// 错误上报省略
return;
}
setState(result)
总结
几种都可以不同适用场景,个人偏好第五种能兼顾前端的编码习惯。
更多阅读
null or undefined?