当前位置:网站首页>js异步错误处理

js异步错误处理

2022-07-05 08:40:00 碳酸金鱼。

引入

我们都知道 try catch 无法捕获 setTimeout 异步任务中的错误,那其中的原因是什么。以及异步代码在 js 中是特别常见的,我们该怎么做才比较?

无法捕获的情况

function main() {
    
  try {
    
    setTimeout(() => {
    
      throw new Error('async error')
    }, 1000)
  } catch(e) {
    
    console.log(e, 'err')
    console.log('continue...')
  }
}
main();

这段代码中,setTimeout 的回调函数抛出一个错误,并不会在 catch 中捕获,会导致程序直接报错崩掉。
所以说在 js 中 try catch 并不是说写上一个就可以高枕无忧了。难道每个函数都要写吗?

那什么情况下 try catch 无法捕获 error 呢?

1. 异步任务

宏任务的回调函数中的错误无法捕获

上面的栗子稍微改一下,主任务中写一段 try catch,然后调用异步任务 task,task 会在一秒之后抛出一个错误。

// 异步任务
var task = () => {
    
  setTimeout(() => {
    
   throw new Error('async error')
 }, 1000)
}
// 主任务
function main() {
    
  try {
    
    task();
  } catch(e) {
    
    console.log(e, 'err')
  }
}

这种情况下 main 是无法 catch error 的,这跟浏览器的执行机制有关。异步任务由 eventloop 加入任务队列,并取出入栈(js 主进程)执行,而当 task 取出执行的时候, main 的栈已经退出了,也就是 上下文环境已经改变 ,所以 main 无法捕获 task 的错误。
事件回调,请求回调同属 tasks,所以道理是一样的。

eventloop 复习可以看这篇 js中的事件循环机制

微任务(promise)的回调

// 返回一个 promise 对象
var promiseFetch = () => 
  new Promise((reslove) => {
    
  reslove();
})
function main() {
    
  try {
    
    // 回调函数里抛出错误
    promiseFetch().then(() => {
    
      throw new Error('err')
    })
  } catch(e) {
    
    console.log(e, 'eeee');
  }
}

promise 的任务,也就是 then 里面的回调函数,抛出错误同样也无法 catch。因为微任务队列是在两个 task 之间清空的,所以 then 入栈的时候,main 函数也已经出栈了。

2. 并不是回调函数无法 try catch

很多人可能有一个误解,因为大部分遇到无法 catch 的情况,都发生在回调函数,就认为回调函数不能 catch。
不全对,看一个最普通的栗子。

// 定义一个 fn,参数是函数。
var fn = (cb: () => void) => {
    
  cb();
};
function main() {
    
  try {
    
    // 传入 callback,fn 执行会调用,并抛出错误。
    fn(() => {
    
      throw new Error('123');
    })
  } catch(e) {
    
    console.log('error');
  }
}
main();

结果当然是可以 catch 的。因为 callback 执行的时候,跟 main 还在同一次事件循环中,即一个 eventloop tick。所以上下文没有变化,错误是可以 catch 的。 根本原因还是同步代码,并没有遇到异步任务。

promise 的异常捕获

构造函数

先看两段代码:

function main1() {
    
  try {
    
    new Promise(() => {
    
      throw new Error('promise1 error')
    })
  } catch(e) {
    
    console.log(e.message);
  }
}
function main2() {
    
  try {
    
    Promise.reject('promise2 error');
  } catch(e) {
    
    console.log(e.message);
  }
}

以上两个 try catch 都不能捕获到 error,因为 promise 内部的错误不会冒泡出来,而是被 promise 吃掉了,只有通过 promise.catch 才可以捕获,所以用 Promise 一定要写 catch 啊。

然后我们再来看一下使用 promise.catch 的两段代码:

// reject
const p1 = new Promise((reslove, reject) => {
    
  if(1) {
    
    reject();
  }
});
p1.catch((e) => console.log('p1 error'));

// throw new Error
const p2 = new Promise((reslove, reject) => {
    
  if(1) {
    
    throw new Error('p2 error')
  }
});
p2.catch((e) => console.log('p2 error'));

promise 内部的无论是 reject 或者 throw new Error,都可以通过 catch 回调捕获。
这里要跟我们最开始微任务的栗子区分,promise 的微任务指的是 then 的回调,而此处是 Promise 构造函数传入的第一个参数,new Promise 是同步执行的。

then

那 then 之后的错误如何捕获呢。

function main3() {
    
  Promise.resolve(true).then(() => {
    
    try {
    
      throw new Error('then');
    } catch(e) {
    
      return e;
    }
  }).then(e => console.log(e.message));
}

只能是在回调函数内部 catch 错误,并把错误信息返回,error 会传递到下一个 then 的回调。

用Promise捕获异步错误

const p3 = () =>  new Promise((reslove, reject) => {
    
  setTimeout(() => {
    
    reject('async error');
  })
});
function main3() {
    
  p3().catch(e => console.log(e));
}
main3();

把异步操作用 Promise 包装,通过内部判断,把错误 reject,在外面通过 promise.catch 捕获。

async/await 的异常捕获

首先我们模拟一个请求失败的函数 fetchFailure,fetch 函数通常都是返回一个 promise

main 函数改成 async,catch 去捕获 fetchFailure reject 抛出的错误。能不能获取到呢。

const fetchFailure = () => new Promise((resolve, reject) => {
    
  setTimeout(() => {
    // 模拟请求
    if(1) reject('fetch failure...');
  })
})
async function main () {
    
  try {
    
    const res = await fetchFailure();
    console.log(res, 'res');
  } catch(e) {
    
    console.log(e, 'e.message');
  }
}
main();

显然是可以的。

更进一步

async 函数处理异步流程是利器,但是它也不会自动去 catch 错误,需要我们自己写 try catch,如果每个函数都写一个,也挺麻烦的,比较业务中异步函数会很多。

首先想到的是把 try catch,以及 catch 后的逻辑抽取出来。

const handle = async (fn: any) => {
    
  try {
    
    return await fn();
  } catch(e) {
    
    // do sth
    console.log(e, 'e.messagee');
  }
}
async function main () {
    
    const res = await handle(fetchFailure);
    console.log(res, 'res');
}

写一个高阶函数包裹 fetchFailure,高阶函数复用逻辑,比如此处的 try catch,然后执行传入的参数-函数 即可。
然后,加上回调函数的参数传递,以及返回值遵守 first-error,向 node/go 的语法看齐。如下:

const handleTryCatch = (fn: (...args: any[]) => Promise<{
    }>) => async (...args: any[]) => {
    
  try {
    
    return [null, await fn(...args)];
  } catch(e) {
    
    console.log(e, 'e.messagee');
    return [e];
  }
}
async function main () {
    
  const [err, res] = await handleTryCatch(fetchFailure)('');
  if(err) {
    
    console.log(err, 'err');
    return;
  }
  console.log(res, 'res');
}

但是还有几个问题,一个是 catch 后的逻辑,这块还不支持自定义,再就是返回值总要判断一下,是否有 error,也可以抽象一下。
所以我们可以在高阶函数的 catch 处做一下文章,比如加入一些错误处理的回调函数支持不同的逻辑,然后一个项目中错误处理可以简单分几类,做不同的处理,就可以尽可能的复用代码了。

// 1. 三阶函数。第一次传入错误处理的 handle,第二次是传入要修饰的 async 函数,最后返回一个新的 function。
const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
  (fn: (...args: any[]) => Promise<{
    }>) => async(...args: any[]) => {
    
    try {
    
      return [null, await fn(...args)];
    } catch(e) {
    
      return [handle(e)];
    }
  }
   
// 2. 定义各种各样的错误类型
// 我们可以把错误信息格式化,成为代码里可以处理的样式,比如包含错误码和错误信息
class DbError extends Error {
    
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    
    super(msg);
    this.errmsg = msg || 'db_error_msg';
    this.errno = code || 20010;
  }
}
class ValidatedError extends Error {
    
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    
    super(msg);
    this.errmsg = msg || 'validated_error_msg';
    this.errno = code || 20010;
  }
}
// 3. 错误处理的逻辑,这可能只是其中一类。通常错误处理都是按功能需求来划分
// 比如请求失败(200 但是返回值有错误信息),比如 node 中写 db 失败等。
const errorHandle = (e: Error) => {
    
  // do something
  if(e instanceof ValidatedError || e instanceof DbError) {
    
    // do sth
    return e;
  }
  return {
    
    code: 101,
    errmsg: 'unKnown'
  };
}   
const usualHandleTryCatch = handleTryCatch(errorHandle);
// 以上的代码都是多个模块复用的,那实际的业务代码可能只需要这样。
async function main () {
    
  const [error, res] = await usualHandleTryCatch(fetchFail)(false);
  if(error) {
    
    // 因为 catch 已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断 if error
    console.log(error, 'error');
    return;
  }
  console.log(res, 'res');
}

解决了一些错误逻辑的复用问题之后,即封装成不同的错误处理器即可。

原网站

版权声明
本文为[碳酸金鱼。]所创,转载请带上原文链接,感谢
https://blog.csdn.net/weixin_43842373/article/details/125595862