浅谈异步编程中错误的捕获

详解awai的异步编程场景

之前的文章说到,async和await可以取代生成器函数和yield的组合,实现优雅的异步操作写成同步写法。​那异步错误的捕获又该如何处理​?这篇文章我​将先讲async和await的特点,然后讲解​异步编程中错误的捕获。

一,async关键字

async关键字标记的函数,会变成异步函数,它的返回值和一般函数不同。

1,返回一个promise
2,返回一个thenable对象则和then方法中的resolve,或者reject有关
1.1,async的返回必然是一个promise

如果返回的是普通值,它会用promise.resolve()把它转化为promise的。

async function test(){
  return 'test'
}
const a=test()
console.log(a)//Promise { 'test' }

如果返回值是promise,那就走正常的promise逻辑,看它异步操作后的结果是成功还是失败。

async function test(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      reject("失败了")
    },1000)
  })
}
const a=test()
a.then((res)=>{
  console.log("成功了执行",res)
},(err)=>{
  console.log("失败了执行",err)//失败了执行 失败了
})
1.2,返回一个thenable对象

如果返回值是一个thenable对象,因为会递归执行其中的then方法,最后返回的promise就是这个thenable对象then方法中处理后的promise。

async function test1(){
  const thenableA = {
       name: '名字哦',
       then: function (resolve, reject) {
           console.log(`I'am ${this.name}`);
           resolve(this.name)
       }
   }
  return thenableA
}
const a=test1()//I'am 名字哦
setTimeout(()=>{
  console.log(a)//Promise { '名字哦' },注意到这里的promise取到了最终的值,而不是这个thenableA对象
})

二,await关键字

1,异步函数中可以使用await关键字,普通函数不行
2,通常await关键字后面都是跟一个Promise,这点和async的返回类似。并且await执行后返回的是该promise处理完成的value值.

await只能在async异步函数中使用,并且它返回的是promsie的结果值。这里可以看之前读取文件内容的代码:

const fs = require('fs')
function readFile(fileName){
    return new Promise((resolve,reject)=>{
        fs.readFile(fileName, (err,data) => {
            resolve(data.toString())
        })
    })
}
async function test() {
    //先按照顺序读取三个文件
   const txt1= await readFile('./text1.txt');
   const txt2= await readFile('./text2.txt');
   const txt3= await readFile('./text3.txt');
   //这里再用结果处理其他代码
   console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
test()

三,async/await的同步写法只是在同个async调用栈内生效

如下代码,成功了22222之后打印。是因为await把代码暂停。只会在async这个test的调用栈中生效。

function test1(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve("成功了")
    },1000)
  })
}
async function test(){
    console.log("3333")
    const a =await test1()
    console.log(a)
}
console.log("11111")
test()
console.log("22222")
//打印结果
//11111
//3333
//22222
//成功了

四,asycn和await让异步操作排队完成与并行完成

4.1,串行完成异步操作

我们常规使用async/await的时候,如下代码,异步任务会按照顺序执行,一个结束之后才会执行下一个,这样会造成该调用栈内的代码非常耗时。

如下代码:

function test1(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(1)
      resolve("成功了第一个")
    },1000)
  })
}
function test2(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(2)
      resolve("成功了第二个")
    },2000)
  })
}
function test3(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(3)
      resolve("成功了")
    },3000)
  })
}
async function test(){
    await test1()
    await test2()
    await test3()
}
test()

这里的test函数中,三个异步操作会按照顺序完成,一个完成后才进行下一个,这就非常耗时,这里将花费6s的时间。在有些时候,我们更希望并行完成异步请求,把这个时间节省下来。

4.2,让异步请求并行完成
4.2.1,利用js的事件循环机制

当几个任务相互独立,没有啥依赖关系时:

function test1(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(1)
      resolve("成功了第一个")
    },1000)
  })
}
function test2(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(2)
      resolve("成功了第二个")
    },2000)
  })
}
function test3(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(3)
      resolve("成功了第三个")
    },3000)
  })
}
async function test(){
  const a=test1()
  const b=test2()
  const c= test3()
  const a1= await a
  const b1= await b
  const c1= await c
}
test()

当我们执行const a=test1();;const b=test2();const c= test3()的时候,因为promise执行构造函数的时候,是同步的,所以这三个异步操作已经在event Table中注册执行,可以理解为三个水壶同时插上电开始烧水了。(这个比喻可以看我这篇文章理解:js事件循环机制-宏任务微任务_笑道三千的博客-CSDN博客

这时候,abc都是状态为pedding的promise。代码继续执行到await,因为await其实效果就是yield一样,会暂停代码的执行,内部使用的是如下:

Promise.resolve(p.value).then(res=>{
   _next(res);
})

这种方式处理,也就是await是调用这个then方法,返回这个promise的结果值(等状态变成fulfilled)。

那么说,三壶水都烧完是只过了3s的时间,第一个await在第一秒后有返回,第二个是接着执行代码,然后到第二个await,接着是第三个。由此实现并行执行异步操作。

4.2.2,利用promise.all方法

另外可以使用promise.all方法来实现,其实原理是一样的。但是它能等这几个并行的异步都完成后再拿结果来统一处理。

function test1(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(1)
      resolve("成功了第一个")
    },1000)
  })
}
function test2(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(2)
      resolve("成功了第二个")
    },2000)
  })
}
function test3(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(3)
      resolve("成功了第三个")
    },3000)
  })
}
async function test(){
  const a=await Promise.all([test1(),test2(),test3()])
  console.log(a)
}
test()

其实这也是利用的js的事件循环机制,我们来看下promise.all的实现原理:

MyPromise.all = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var count = 0;
    var result = [];
    var length = promiseList.length;
    if(length === 0) {
      return resolve(result);
    }
    promiseList.forEach(function(promise, index) {
      MyPromise.resolve(promise).then(function(value){
        count++;
        result[index] = value;
        if(count === length) {
          //全部执行完毕后才resolve结果数组出去
          resolve(result);
        }
      }, function(reason){
        reject(reason);
      });
    });
  });
  return resPromise;
}

可以看到其内部也是使用的:

MyPromise.resolve(promise).then(function(value){
    count++;
    result[index] = value;
    if(count === length) {
        //全部执行完毕后才resolve结果数组出去
        resolve(result);
    }
}, function(reason){
    reject(reason);
});

只是遍历数组,把所有的异步操作都执行一遍。

这两种方法其实都有一些问题。看promise.all的实现原理就能明白。当并行的任务中有一个失败后,是直接reject的,也就是返回的promise是reject的状态。结果只也是错误的信息。

对于错误的捕获,下节再讲。

4.2.3,promise.all实现并发请求并控制数量

这个来自于常见的面试题:

实现一个并发请求函数concurrencyRequest(urls, maxNum,callback),要求如下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,可以增加新的请求
• 所有请求完成后,结果按照 urls 里面的顺序依次打出,可以使用回调函数处理最后的结果
const sendRequest=(tasks,max,callBack)=>{
    let index=0;
    let limitPool=new Array(max).fill(null);
    const result=[];
    //这个map控制着并行的数量,因为map这里是同步的代码,所以异步任务进入eventLoop中执行,每次正好是这个并行数量
    const limitResult=limitPool.map(()=>{
        return new Promise((resolve)=>{
            const run=()=>{
                //因为该条并行线始终没有resolve,所以这个promsie的状态没有改变
                if(index>=tasks.length){
                    resolve()
                    return
                }
                let cur=index
                let task=tasks[index++]
                //递归,从而在当前异步完成/失败后继续执行剩下的
                task().then((res)=>{
                    result[cur]=res
                    run()
                }).catch((err)=>{
                    result[cur]=err
                    run()
                })
            }
            run()
        })
    })
    //promsie.all只会在limitResult,这几条并行的线都完成之后,才可以执行then方法,而then方法这时候就可以取最后的结果啦。
    Promise.all(limitResult).then(()=>callBack(result))
}

//开始使用
function asyncCreate(num){
    let arr=[]
    const asyncFn=function(){
        return new Promise((resolve)=>{
            setTimeout(()=>{
                console.log("代码中异步执行")
                resolve("异步完成")
            },1000)
        })
    }
    for(let i=0;i<num;i++){
        arr.push(asyncFn)
    }
    return arr
}
const funtionList=asyncCreate(10)
sendRequest(funtionList,4,res=>{
    console.log(res)
})

本质上就是使用的事件循环机制注册异步事件,换汤不换药,只不过多了个并行池的概念,利用limitPool.map来控制这个并行池中并行线的数量,而promsie.all等待的则是这个并行池的几个并行线都执行结束

另一种更简洁的方法是利用await停住代码,等并行池中有请求完成了再继续往并行池中添加请求,当最后还是利用promise.all()来保证所有请求全部完成:

async function sendRequest(tasks,limit,callback){
    const promises=[]
    const pool= new Set()
    for(const task of tasks){
        const promise=task()
        promises.push(promise)//构建结果集
        pool.add(promise)//构建并行池
        const clean=()=>pool.delete(promise)
        promise.then(clean,clean)//每个异步请求完成后,自动从并发池中清除
        if(pool.size>=limit){//并发池的并行数量等于限制数量时,使用await停住代码,直到并发池有一个请求完成
            await Promise.race(pool)
        }
    }
    Promise.all(promises).then((res)=>{callback(res)})//所有的请求完成后执行回调函数取结果
}

//开始使用
function asyncCreate(num){
    let arr=[]
    const asyncFn=function(){
        return new Promise((resolve)=>{
            setTimeout(()=>{
                console.log("代码中异步执行")
                resolve("异步完成")
            },1000)
        })
    }
    for(let i=0;i<num;i++){
        arr.push(asyncFn)
    }
    return arr
}
const funtionList=asyncCreate(10)
sendRequest(funtionList,4,res=>{
    console.log(res)
})

五,try……catch的错误捕获

5.1,promsie的同步异常捕获

上文一直没说到异常的捕获,当我们使用promsie的时候,,如果一个 Promise 操作发生了异常,那么它将会被拒绝,此时它的状态会变成 rejected。你可以使用then方法或者catch处理错误。

function asyncFunc() {
  return new Promise((resolve, reject) => {
      reject('Error from asyncFunc');
  });
}
const fn=res=>console.log("---",res)
asyncFunc().then(null,fn);   //--- Error from asyncFunc

或者采用catch:

function asyncFunc() {
  return new Promise((resolve, reject) => {
      reject('Error from asyncFunc');
  });
}
const fn=res=>console.log("---",res)
asyncFunc().catch(fn) //--- Error from asyncFunc

这是因为catch实际上是处理状态变成rejected的promise,其调用的是then的onRejected方法。所以catch也是微任务

MyPromise.prototype.catch = function(onRejected) {
  this.then(null, onRejected);
}

另外,在promise中直接throw new Error也是能被catch捕获到的:

function asyncFunc() {
  return new Promise((resolve, reject) => {
      throw new Error('Error from asyncFunc');
  });
}

asyncFunc().catch((err) => {
  console.error('Caught error:', err.message);//Caught error: Error from asyncFunc
});

但是按照刚刚的说法,catch不是要等promise的状态变成reject之后,才能使用catch捕获到错误吗?那这里为啥又能捕获到?

这是因为promise内部执行函数(executor)执行的时候,是这样封装的(具体可以看我这篇文章:https://juejin.cn/post/7218178695679410231#heading-21):

function MyPromise(fn) {
    ...//其他代码
    try {
        fn(resolve, reject);
    } catch (error) {
        reject(error);
    }
}

当在promsie中直接throw new Error的时候,会被catch捕获到,从而执行reject方法,将promise的状态修改为rejected,继而能被catch所捕获。

其实再深一层,于 JavaScript 异常处理机制:当 throw 语句抛出异常时,JavaScript 引擎会在当前作用域中查找能够处理这个异常的代码,如果找到了 try 块内的 catch 块,那么就会进入 catch 代码块去处理这个异常;否则会将异常抛给上一层作用域,直到全局作用域。

5.2,promsie的异步异常捕获

上文的throw new Error如果修改为如下代码,将其放到一个异步操作中执行:

function asyncFunc() {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      throw new Error('Error from asyncFunc');
    },1000)
  });
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);

会发现又无法捕获到这个错误,也就是没有执行console.error('Caught error:', err.message);这行代码。

这又是为什么呢?

这得从事件循环机制和promise的原理(具体可以看我这篇文章:https://juejin.cn/post/7218178695679410231#heading-21)说起。

1,首先promise执行函数(executor)执行,遇到setTimeout是宏任务,进入宏任务队列。
2,执行asyncFunc().catch方法,上文说过,它内部调用的是promise的then方法,是个微任务,进入微任务队列
3,这个微任务先执行完成,这个catch实际上做的事情就是将它的回调函数fn封装下再push到promise的回调函数收集器里面等待执行。
4,这时候,宏任务setTimeout执行完毕,开始执行它的回调:throw new Error,它是无法被promise的执行函数(executor)的try……catch捕获的。(这个下文会讲到)。于是这个promsie的状态始终是pedding,也就自然不会执行收集器里面的回调函数,不会执行console.error('Caught error:', err.message)。

那promise的异常捕获应该如何处理呢?

实际上文已经说了。就是异步的错误使用reject来 包裹,这样处理的原理是使用闭包,利用的是promise执行reject会变更状态,同时取出收集器中的回调函数依次执行。如下代码:

function asyncFunc() {
    return new Promise((resolve, reject) => {
      setTimeout(()=>{
        reject(new Error('Error from asyncFunc'));
      },1000)
    });
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);//Caught error: Error from asyncFunc
5.3,try…catch不能捕获的错误类型

上文其实已经遇到了,有些错误是try…catch无法捕获到的,对于上文的异步操作我们利用了promise的catch来处理。那具体都有哪些错误是try…catch无法捕获的呢?

一句话总结就是:能捕捉到的异常必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的

5.3.1 直接的语法错误不能被捕获

因为语法错误是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常。

try{
  a.b
}catch(err){
  console.log(err)
}
//ReferenceError: a is not defined
5.3.2 异步无法捕获
try{
    setTimeout(()=>{
         console.log(a.b);  
    }, 100)
}catch(e){
    console.log('error',e);
}
console.log(111);
//output
111
Uncaught ReferenceError: a is not defined

因为,setTimeout是异步函数,而try catch其实是同步顺序执行的代码,等setTimeout里面的事件进入事件队列的时候,主线程已经离开了try catch,所以try catch是无法捕获异步函数的错误的。

5.3.3 多层try…catch,如果内层捕获到的错误未上抛,则上层无法捕获

多层 try-catch 时,会被最内层的 catch()方法捕获到,然后就不再向外层冒泡:

try {
  try {
    throw new Error('error');
  } catch (err) {
    console.error('内层的catch', err); // 内层的catch Error: error
  }
} catch (err) {
  console.error('最外层的catch', error);
}
5.3.4 promsie对象包裹的错误无法捕获

try catch无法捕获promise对象的错误。

function asyncFn(){
  return new Promise((resolve,reject)=>{
    throw new Error("错误了哈")
  })
}
function test(){
  try{
    asyncFn()  
  }catch(err){
    console.log("成功捕获到错误了",err)
  }
}
test()// UnhandledPromiseRejectionWarning: Error: 错误了哈

这是因为promise的执行函数(executor)执行的时候,是使用try…catch包裹的,这个上文5.1最后部分也说过了,它是catch到报错之后再reject(err),并没有把错误往上层抛,结合5.3.3来看,promise实际上并没有把错误往上抛,所以外层的try…catch无法捕获到这个错误。

接下来再来看5.2中的promise中的异步错误:

function asyncFunc() {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      throw new Error('Error from asyncFunc');
    },1000)
  });
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);

因为setTimeout在执行回调时,主线程早已经离开了promise内置的try…catch,所以并没有被它所捕获,promise的状态也就没有发生变更,更不会执行收集器中then的回调函数。

5.4 异步操作的错误捕获写法总结

为了能够捕获到异步操作的错误,总结起来,就是如下写:

5.4.1 仅使用pramise.catch时,手动reject错误
function asyncFunc() {
    return new Promise((resolve, reject) => {
      setTimeout(()=>{
        reject(new Error('Error from asyncFunc'));
      },1000)
    });
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);//Caught error: Error from asyncFunc
5.4.2 使用try…catch时,需要await

await起到的作用就是主线程停留在try…catch中,从而捕获错误。

function asyncFn(){
  return new Promise((resolve,reject)=>{
    throw new Error("错误了哈")
  })
}
async function test(){
  try{
    await asyncFn()  
  }catch(err){
    console.log("成功捕获到错误了",err)
  }
}
test()//成功捕获到错误了 Error: 错误了哈

这里有个地方需要和上文联系。之前我们说过promise内部执行构造函数的时候,是如下代码:

function MyPromise(fn) {
    ...//其他代码
    try {
        fn(resolve, reject);
    } catch (error) {
        reject(error);
    }
}

这里使用了内层try…catch捕获了错误,返回的应该是reject状态的promise,并没有把错误往上层抛,理应外层的try…catch无法捕获这个错误。那现在为啥又可以捕获呢?这是因为await的效果:如果它后面是rejected状态的promise,await 表达式也会抛出错误

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值