async/await 错误机制详解,如何优雅地捕获错误

516 阅读4分钟

其实async函数的错误机制和一般的错误非常像

  1. 当一个async函数中出现未捕捉的promise rejecrt,或一般的错误,都会自动中断,并向上抛出reject

比如这里runPromise函数中没有捕捉错误,此时错误会继续向上传播,并能在test处被捕获

test()
async function test() {
    try{
        await runPromise()
    }catch(err){
        console.log('catch')
    }
}
async function runPromise() {
    await rejectPromise();

    // 运行会在这里中断,并向上抛出reject
    console.log('done');
}

async function rejectPromise() {
    return  Promise.reject('error')
}
  

或者是发生一般的错误,也会向上抛出reject,并最后在test处被捕获

test()
async function test() {
    try{
        await runPromise()
    }catch(err){
        console.log('catch')
    }
}
async function runPromise() {
    await errorPromise();

    // 运行会在这里中断,并向上抛出reject
    console.log('done');
}
async function errorPromise(){
    JSON.parse('aaa')  // 这里会抛出异常,并抛出reject
}

  1. 若async函数中捕获了错误,则不会再继续向上传播错误

比如下面例子,test函数中将捕获不到错误

test()
async function test() {
    try{
        await runPromise()
    }catch(err){
        console.log('catch')
    }
}
async function runPromise() {
    try{
        await rejectPromise();

        // 运行会在这里中断,但不会向上抛出reject
        console.log('done');
    }catch(err){
        console.log('reject')
    }
}

async function rejectPromise() {
    return  Promise.reject('error')
}
  1. 若调用async函数时,没有使用await,则不会捕获到错误,错误也不会向上传播

下面例子中,runPromise运行不会中断,test也不会捕获到错误

test()
async function test() {
    try{
        await runPromise()
    }catch(err){
        console.log('catch')
    }
}
async function runPromise() {
    rejectPromise();
    console.log('done');
}

async function rejectPromise() {
    return  Promise.reject('error')
}

最佳实践

对于被调函数而言:

  1. 如果我们希望将错误交给调用者解决,则不应该使用try/catch,让js自动抛出错误
  2. 若我们需要处理错误,则必须在catch处手动返回reject或抛出错误,否则调用者无法捕捉
  3. 调用async函数时一定要使用await,否则错误无法传播

优雅地调用async

若我们不希望代码中出现UnhandledPromiseRejectionWarning的错误时,我们需要对每一个async函数都包一个try/catch,有什么办法可以优化它

这个例子是一个点击事件:点击删除按钮后调用删除接口,若成功则给出成功的提示,失败则给出失败的提示

async function onDeleteBtnClick(id) {
    try{
        await deleteSomethingAPI(id)
        console.log('删除成功')
    }catch(err){
        console.log('删除失败')
    }
}

这个方法有两点不好的地方:

  1. 业务逻辑和错误处理的逻辑放在一起,使得用户需要同时关注两种情况的分支
  2. 事件函数不应该包含具体的业务逻辑,这样会导致事件与具体的业务耦合,不利于业务逻辑的复用和测试

第一步我们要做的是:分离业务逻辑和错误处理逻辑
我们进行以下修改,deleteSomething只关心成功时的业务逻辑,错误处理交由onDeleteBtnClick处理

async function onDeleteBtnClick(id) {
    try{
       await deleteSomething(id)
    }catch(err){
        console.log('删除失败')
    }
}
async function deleteSomething(id){
    await deleteSomethingAPI(id)
    console.log('删除成功')
}

此时onDeleteBtnClick可以优化为这样,于是我们消灭了try/catch

function onDeleteBtnClick(id) {
    deleteSomething(id).catch(err => {
        console.log('删除失败')
    })
}

进一步优化: 使用专门的函数来捕捉错误

我们可以将onDeleteBtnClick的try/catch抽出,处理成一个专门处理错误的函数,并将错误交由统一的errorHandler处理

async function promiseRunner(promise){
    let arg = Array.prototype.slice.call(arguments,1)
    try{
        return await promise(...arg)
    }catch(err){
        return errorHandler(err)
    }
}

function errorHandler(err){
    console.log(err)
}

针对上面的例子,可以写一个能自动给出提示信息的函数来运行async函数,比如这样

import { Message } from 'element-ui';
async function promiseRunnerWithMessage(promise) {
    try{
         let arg = Array.prototype.slice.call(arguments,1)
         const data = await promise(...arg);
         Message.success("操作成功");
         return data;     
    }catch(err){
         Message.error('操作失败')  
         return errorHandler(err)
    }
}

最后onDeleteBtnClick可以优化成这样
而且也发现,事件函数和具体业务完成了解耦

function onDeleteBtnClick(id) {
    promiseRunnerWithMessage(deleteSomethingAPI,id)
}

总结

  1. 将方法中的业务逻辑与错误处理逻辑分离,有利于对两种情况分别进行处理
  2. 使用专门的方法进行错误逻辑的处理,把注意力专注于业务逻辑上

补充:
可以发现promiseRunner并没有返回类似标志位的东西,意味着外层无法判断promiseRunner的运行情况。但这就是我想要的效果。若外层还需要对标志位进行判断,则说明错误分支和业务分支又再次纠缠在一起。因此promiseRunner应该作为最顶层的函数调用,后续不应包含任何逻辑