JS 错误处理 5 种模式

249 阅读3分钟

背景

下面这段代码有何改进之处?

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?