JS异步错误处理解决方案

1,520 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

在之前的文章中,我们详细地复习了JS错误捕获的原理部分,知道了如何使用try…catch…和Promise.catch,那么现在就让我们看一下稍微复杂的场景吧! 虽说是复杂的场景,但其实也是非常常见的,那就是在一个函数中有多个继发的异步操作,对这些异步操作要进行错误捕获。用代码的描述如下:

async function stepOne() {
 // 异步操作一
}

async function stepTwo() {
 // 异步操作二
}

async function stepThree() {
 // 异步操作三
}

// 使用 try...catch...对错误统一捕获
async function asyncRun() {
 try{
  const res1 = await stepOne()
  const res2 = await stepTwo()
  const res3 = await stepThree()
 }catch(err) {
  console.log(err)
 }
}

// 使用 Promise.catch 对错误进行捕获
function runPromises() {
 stepOne()
 .then(res => {
  stepTwo().then(res => {
   stepThree().then(res => {})
  })
 })
 .catch(err => {
  console.log(err)
 })
}

在上面的代码示例中分别使用了try…catch…和Promise.catch对错误进行统一捕获,利用了错误抛出后的冒泡机制。我们暂且不谈这两种形式的优劣,想想我们的目的,假设要对这些错误进行识别,针对不同的异步操作的错误做不同的处理,这种情况下,简单使用一个try…catch…或者是Promise.then是不能实现的。

最容易想到的解决方法就是使用多个try…catch…进行嵌套,虽然但是,有没有更优雅一点的办法呢。

在我阅览了网上的一些解决方法后,有了下面几种方式的总结,其中有一定相似之处,我将从简单的开始一一介绍。

v1.0 await doSomething().catch(e=>e)

async function runAsync() {
 const res1 = await stepOne().catch(e => e)
 // 对res1的类型或值进行判断
 if(...) {
  // 如果出错
  // 错误处理逻辑
 }
 const res2 = await stepTwo().catch(e => e)
 // 对res2的类型或值进行判断
 if(...) {
  // 如果出错
  // 错误处理逻辑
 }
}

这种方式非常直接,使用Promise.catch方法将错误捕获并返回一个resolved状态且值为error的promise。

如果出错的话,res1的值就是error,所以在此处一般要使用条件语句对res1进行判断再接下来的操作。

v1.1 error first

使用返回错误优先原则,这里的书写风格和Node.js和Golang是一样的,也是前一种写法的加强版,将值和错误使用数组一并返回。

async function runAsync() {
 const [err1, res1] = await stepOne(val => [null, val]).catch(e => [e, null])
 if(err1) {
  // 如果出错
  // 错误处理逻辑
 }
 const [err2, res2] = await stepTwo(val => [null, val]).catch(e => [e, null])
 if(err2) {
  // 如果出错
  // 错误处理逻辑
 }
}

这种写法是比较实用的,如果我们的项目中也就这一两个地方有这种继发的异步操作并且需要错误处理的话够用了。当然,我们很快会发现,这种方式带来了很多重复的地方,是不符合DRY原则的。比如:

  • 每一个await表达式后面都需要封装成数组返回
  • 每一个异步操作都要进行一次条件语句的判断

在v2的版本中,我们会进行适当的封装~

v1.2 在catch中处理错误并继续抛出

这种方式有适用于对错误有处理逻辑,同时会终止当前函数的执行(return)这类场景。类似于v1.1版本中在条件语句中,处理错误并使用 return。

async function runAsync() {
 try{
 const res1 = await stepOne().catch(err => {
  // 处理错误1
  // handleErr(err)
  // 继续抛出错误
  Promise.reject(err)
 })
 const res2 = await stepTwo().catch(err => {
  // 处理错误2
  // handleErr(err)
  // 继续抛出错误
  Promise.reject(err)
 })

 }catch(err) {
  // 统一的错误处理逻辑
 }
}

在此处通过调用 Promise.reject 静态方法,可以将该异步操作的错误继续抛出,从而不执行后续的逻辑,类似于 if(err)return 的方式。当然,这种方式在使用typescript的时候,对res1的返回值类型需要一些处理。

v2.0 抽离 try…catch…的逻辑

如果在项目中有多处上述情景,那么就需要进行适当的封装了,不然的话,我们也只是别扭的把嵌套的 try…catch… 转换成了额外的条件判断+return语句。

首先想到的就是将其抽成一个函数。

const handle = (fn: (...args: any[]) => Promise<{}>) => async (...argsany[]) => {
  try {
    return [nullawait fn(...args)];
  } catch(e) {
    console.log(e, 'e.messagee');
    return [e];
  }
}

async function runAsync() {
 const [err1, res1] = handle(stepOne)
 if(err1) {
  // 错误处理
  // return
 }
 const res2 = handle(steoTwo)
}

上面的代码展示的是一个简单版本的 handle 函数,可以根据需求,使用 Error-first 规则等自行封装 handle 函数,在调用异步操作时,调用handle进行包裹。

但其实 😅,这种封装时比较鸡肋的,因为在 handle 函数的catch子句中只有统一的错误处理的逻辑,如果需要针对性的处理,那么还是避免不了在主函数中的二次判断及处理。

v2.1 自定义错误类型

这个想法参考了这篇文章 👇

JS 异步错误捕获二三事

通过自定义错误类型+封装错误处理器解决处理上个版本中的handle函数无法进行错误类型识别的问题。当然,这里的实现方式涉及到了高阶函数和继承Error构造自定义错误对象,可能稍显复杂一些。

但是,这里对错误类型的扩展方式还是值得我们学习的。


class DbError extends Error {
  public errmsgstring;
  public errnonumber;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'db_error_msg';
    this.errno = code || 20010;
  }
}
class ValidatedError extends Error {
  public errmsgstring;
  public errnonumber;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'validated_error_msg';
    this.errno = code || 20010;
  }
}

在此基础上,作者还引申出了使用装饰器的处理方式,用于简化高阶函数的逻辑,有兴趣的小伙伴可以进一步探索 🤩~

v2.2 抽离错误处理逻辑为装饰器/loader

刚刚的做法是将错误处理逻辑抽离为函数,还有一些方式对代码的侵入性更小,比如抽离为loader。下面这个作者就是结合 babel 进行解析和插入,其主要思想就是将遍历语法树时遇到的await表达式的节点前插入try…catch…代码块进行处理。链接如下 👇

异步错误处理loader

这种方式对代码书写的风格统一性提高了,侵入性降低了,但同时灵活性降低了。并且,在实现此类loader时需要针对不同的语境做更多的兼容考虑。

因此,结合具体的业务场景,我们还是要具体分析 😜.

最后,总结一下。

从简单的处理方式到封装性更好的解决方案,其实还是那句话,要结合业务场景来看。没有最优解,甚至有时候写 try…catch…嵌套(三个以下)也不失为一种简洁的方法 😉。