Promise 和 async/await 的异常处理最全整理

705 阅读9分钟

先看一个典型的例子

function getList(){
    return new Promise((rs,rj)=>{
        rj('假装发生了错误')
    })
}

try{
    getList();
}
catch(e){
    alert('遇到了些许错误哦~')
}

使用try...catch...捕获异常,发生错误时显示友好的提示。

emmm……可是实际会发生什么呢?

答案是,此处的异常将无法捕获到! 当getList()抛出异常后,嗯,流程就失控了。

为什么哪????我百思不得其解??

那就一步一步来研究研究,我看看是怎么个事!

Promise 和 async/await 的区别

1.语法层面的差别:

async/await提供了一种更接近同步代码的写法,使得异步代码的可读性和可维护性得到提高。在async函数中,如果遇到await表达式后面跟着的Promise被拒绝(rejected),它会抛出一个异常。

Promise本身是一个对象,它代表了一个异步操作的最终完成或失败。Promise的异常是通过.catch()方法或者.then()方法中的第二个参数函数来捕获的。

2.异常传播方式:

在async函数中,如果抛出异常,这个异常会向上冒泡,直到被捕获。这意味着你可以在async函数的外部使用try...catch来捕获异常。

对于Promise,你需要显式地使用.catch()或者在链式调用的.then()中处理异常,否则未捕获的异常会导致程序崩溃。

3.错误处理的位置:

使用async/await时,错误处理通常发生在try...catch块中,这使得错误处理的位置更加灵活,可以放在逻辑流程的任何地方。

使用Promise时,错误处理通常发生在链的末尾或者每个.then()调用之后,这可能会使得代码链变得冗长,尤其是在多个异步操作需要顺序执行时。

4.错误处理的链式调用:

在async/await中,你可以使用try...catch来捕获单个await表达式抛出的异常,也可以捕获整个async函数中的异常。

在Promise中,错误处理通常是链式进行的,每个.then()后面都可以跟随一个.catch()来处理前一个.then()中可能发生的错误。

5.错误处理的简洁性:

async/await因为其语法的简洁性,使得错误处理看起来更直观,更像同步代码。

Promise的错误处理可能需要更多的代码,尤其是在多个.then()调用链中,每个都需要单独的错误处理。

6.错误传播的透明度:

在async/await中,如果一个await表达式抛出异常,这个异常可以被async函数外部的try...catch捕获,这提高了错误传播的透明度。

在Promise中,如果链中的某个.then()没有正确处理异常,那么这个异常可能不会被立即捕获,直到链的末尾或者被显式地捕获。

在async函数中,如果抛出异常,这个异常会向上冒泡,直到被捕获。这意味着你可以在async函数的外部使用try...catch来捕获异常。

对于Promise,你需要显式地使用.catch()或者在链式调用的.then()中处理异常,否则未捕获的异常会导致程序崩溃。

Promise的异常捕获方式

打开控制台会发现,上面的例子将会抛出****Uncaught (in promise)异常。

这里要提到Promise的异常处理流程。

Promise创建时需要传入一个function,在这个function执行过程中,如果出现了异常则会对外抛出。 外部有2种方式来捕获这个异常:

方式一:catch()

let promise = new Promise(...);
promise.catch(e=>{
  // TODO sth with e
})

方式二:async/await中的try...catch...

let promise = new Promise(...);
async function test(){
  try{
    await promise;
  }
  catch(e){
    // TODO sth with e
  }
}

如果异常既没有被方式一捕获又没有被方式二捕获,那么异常会被抛到全局中。

在NodeJs中可以通过process.on('unhandledRejection', e => {...})捕获全局异常

在浏览器中目前没有有效的方法可以捕获全局异常,所以我们要尽可能的去避免全局异常,全局异常通常会引起流程中断。

好了现在我们知道了如何捕获,那我就捕获一下吧:

function getList(){
    return new Promise((rs,rj)=>{
        rj('假装发生了错误')
    })
}

async function main(){
    try{
        getList();
    }
    catch(e){
        alert('遇到了些许错误哦~')
    }
}

main();

欧吼!发现又有问题了,还是没有被捕获,这是为什么呢?

带着疑问继续往下⬇️

async/await 到底是什么?

简单来说,async/await 是以更舒适的方式使用 promise 的一种特殊语法,本质上就是promise。

让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:

async function f() {
  return 1;
}

在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

上面的代码转化一下

// async function f() {
//   return 1;
// }

// function getList(){
//     return new Promise((rs,rj)=>{
//         rj('假装发生了错误')
//     })
// }

// async function main(){
//     try{
//         getList();
//     }
//     catch(e){
//         alert('遇到了些许错误哦~')
//     }
// }

// main();
async function f() {
  return Promise.resolve(1);
}


function main(){
    return Promise.resolve(XXXXXXX)
}

我们知道async表示这段代码是一个异步执行代码,在async中,await会阻碍后面代码的执行。如果没有await,代码就不会被阻塞,会以同步的方式进行。在js中,async和await是成对出现的,去思考一下,假如await单独出现会是什么情况?答案是后面的全部都会中断了,所以js中不允许单独出现。

这里给出一个await的例子

// 只在 async 函数内工作
let value = await promise;

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

这里的例子就是一个 1 秒后 resolve 的 promise:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。

让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写。

不能在普通函数中使用 await

如果我们尝试在非 async 函数中使用 await,则会报语法错误:

function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

如果我们忘记在函数前面写 async 关键字,我们可能会得到一个这个错误。就像前面说的,await 只在 async 函数中有效。

未捕获异常的原因

例子中的Promise异常未被正常捕获,是因为promise虽然出现在try...catch...中,但是并没有被await,如此将不进入上述的异常捕获流程,一旦出现异常并且没有其它有效的catch时,就将抛出至全局。

new Promise(rs=>{
    throw new Error('Error')
}).catch(e=>{
    console.log('异常被捕获到了1')
})

new Promise(async rs=>{
    throw new Error('Error')
}).catch(e=>{
    console.log('异常被捕获到了2')
})

👆上面两个🌰可以被捕获吗?

根据Promise的链式调用的原则,来分析以上代码,第一个例子是Promise标准捕获异常的方法,所以第一个是可以被捕获到的。

第二个我们拆开来分析

new Promise(rs=>{
    // async 相当于同步函数里又包了一层Promise
    return new Promise(()=>{
        // 内层Promise抛出异常
        throw new Error('Error')
    })
}).catch(e=>{  // 这里catch的是外层Promise
    // 由于异常并未向上抛给外层Promise,所以此处catch不到
    console.log('异常被捕获到了2')
})


new Promise(rs=>{
    // async 相当于同步函数里又包了一层Promise
    // Do sth
    return new Promise(()=>{
        // 内层Promise抛出异常
        throw new Error('Error')
    })
}).catch(e=>{  // 这里catch的是外层Promise
    // 由于异常并未向上抛给外层Promise,所以此处catch不到
    console.log('异常被捕获到了2')
})

链式的调用并不会像async/await一样会被统一处理,也不会层层上报,而是一旦未被捕获就马上会被上升为全局,接着整个流程就会被中断。

ok相信你已经明白了这两个的区别,下面我们接着讨论一下混用的情况。

async function getList(){
   return new Promise((rs,rj)=>{
        rj('假装发生了错误')
    });
}

async function main(){
    try{
        await getList().catch(e=>{
            console.log('异常捕获到了,位置1')
        });
    }
    catch(e){
        console.log('异常捕获到了,位置2')
    }
}

main();

这种情况最后会打印出什么呢?

答案如图

还是用刚才的结论,await会等待代码的执行,getList首先返回一个Promise,状态为reject,用于链式调用原则reject状态会在相同代码层级中被.catch,await后面的代码全部执行完毕,阻塞结束,由于err已经被捕获所以位置2不会再被捕获了。

补充

1.try catch 只能捕获当前上下文中的错误,也就是只能捕获同步任务的情况,如下场景:

try {
    throw "程序执行遇到了一些错误";
} catch(e) {
    console.log(e)
}
// 控制台会输出:程序执行遇到了一些错误

对于异步的任务,try catch就显得无能为力,不能正确捕获错误:

try {
    setTimeout(() => {
      throw "程序执行遇到了一些错误"  
    })
} catch(e) {
    console.log(e);
}
// 控制台输出:Uncaught 程序执行遇到了一些错误;

try {
    Promise.reject('程序执行遇到了一些错误');
} catch(e) {
    console.log(e);
}
// 控制台输出:Uncaught (in promise) 程序执行遇到了一些错误

2.then方法中的第二个参数和Promise.catch方法的区别

new Promise((resolve,reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000)
}).then(res => {
    console.log(res);
    return new Promise((resolve,reject) => {
        reject('第一个then方法报错了');
    })
}, err => {
    console.log(err);
}).then(res => {
    console.log(res);
    return new Promise((resolve,reject) => {
		reject('第二个then方法报错了');
    })
}, err => {
    console.log(err) // 输出:'第一个then方法报错了'
}).catch(err => {
    console.log(err); // 可以捕获整个promise调用链的错误
})

参考文献:

juejin.cn/post/698208…

zh.javascript.info/async-await

blog.csdn.net/dyw3390199/…

segmentfault.com/a/119000000…

juejin.cn/post/711353…