23

如何在使用async & await 时优雅的处理异常

 4 years ago
source link: https://juejin.im/post/5dd1498df265da0bd315cca8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2019年11月17日阅读 3471

如何在使用async & await 时优雅的处理异常

原文来自: blog.grossman.io/how-to-writ…

在ES7的中,我们可以使用async & await进行编写异步函数,使用这种写法我们的异步函数看起来就跟同步代码一样。

在之前的版本(ES6),可以使用Promise写法,来简化我们异步编程的流程,同时也避免了回调地狱

回调地狱是语义化产生的一个术语,它的释义可以用下面这种情况进行阐述:

function AsyncTask() {
   asyncFuncA(function(err, resultA){
      if(err) return cb(err);

      asyncFuncB(function(err, resultB){
         if(err) return cb(err);

          asyncFuncC(function(err, resultC){
               if(err) return cb(err);

               // And so it goes....
          });
      });
   });
}
复制代码

上例代码中, 不断的回调,使得代码维护和管理控制流程变得十分的困难。 我们不妨考虑下这种情况,假如某个if语句需要执行其他的方法,而回调函数FunctionA的结果为foo。

使用Promise优化

ES6和Promise的出现,使得我们可以简化之前"回调地狱"般的代码如下:

function asyncTask(cb) {
   asyncFuncA.then(AsyncFuncB)
      .then(AsyncFuncC)
      .then(AsyncFuncD)
      .then(data => cb(null, data)
      .catch(err => cb(err));
}
复制代码

这样编写是不是看起来舒服多了?

但是在实际的业务场景中,异步流的处理可能会更加复杂一些。举例来说,

假如在你的一个(node.js)服务器中,你可能想要:

  1. 将一个数据1保存到数据库中 (步骤1)
  2. 根据保存的数据1查找另外一个数据2 (步骤2)
  3. 如果查找到了数据2,执行其他的一些异步任务 (其他任务)
  4. 等到所有的任务全部执行完成之后,你可能需要使用你在(步骤1)中得到的结果用来反馈给用户。
  5. 假如在执行任务的过程中发生了错误,你得要告诉用户在哪个步骤发生了错误。

在使用了Promise语法后,这样当然看起来更加的简洁了,但是,在我看来仍然有一点混乱。

ES7 Async/await

您需要使用转译器才能使用Async/Await,您可以使用babel插件或Typescript来添加所需的工具。

此时, 若使用 async/await, 你会发现代码写起来舒服多了. 它允许我们像下面一样编写代码:

async function asyncTask(cb) {
    const user = await UserModel.findById(1);
    if(!user) return cb('No user found');
    const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    
    if(user.notificationsEnabled) {
         await NotificationService.sendNotification(user.id, 'Task Created');  
    }
    
    if(savedTask.assignedUser.id !== user.id) {
        await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
    }
    
    cb(null, savedTask);
}
复制代码

上面的代码看起来可读性增强了不少, 但是如何处理错误报错呢?

在执行异步任务使用Promise的时候可能会发生一些错误类似数据库连接出错,数据库模型验证错误等情况。

当一个异步函数正在等待Promise返回值的时候,当Promise方法报了错误的时候,它会抛出异常,这个异常可以在catch方法里面捕获到。

在使用Async/Await时,我们通常使用try/catch语句进行异常捕获。

try{
    //do something
}
catch{
   // deal err
}
复制代码

我没有编写强类型语言的背景,因此增加额外的try/catch语句, 对我来说增加了额外的代码,这在我看来非常的冗余不干净。 我相信这可能是个人喜好的原因,但这是我对此的看法。

所以之前的代码看起来像这样:

async function asyncTask(cb) {
    try {
       const user = await UserModel.findById(1);
       if(!user) return cb('No user found');
    } catch(e) {
        return cb('Unexpected error occurred');
    }

    try {
       const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    } catch(e) {
        return cb('Error occurred while saving task');
    }

    if(user.notificationsEnabled) {
        try {
            await NotificationService.sendNotification(user.id, 'Task Created');  
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    if(savedTask.assignedUser.id !== user.id) {
        try {
            await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    cb(null, savedTask);
}
复制代码

最近我一直在使用go-lang进行编码,并且非常喜欢他们的解决方案,它的代码看起来像这样:

data, err := db.Query("SELECT ...")
if err != nil { return err }
复制代码

我认为它比使用try/catch语句块更加简洁,并且代码量更少,这使得它可读和可维护更好。

但是使用Await的话,如果没有为其提供try-catch处理异常的话,当程序发生错误的时候,它会默默的退出(你看到不抛出的异常)。假如你没有提供catch语句来捕捉错误的话,你将无法控制它。

当我和Tomer Barnea(我的好朋友)坐在一起并试图找到一个更简洁的解决方案时,我们得到了下一个使用方法: 请记住: Await在等待一个Promise返回值

async & await 异常捕捉工具函数

有了这些知识,我们就可以制作一个小的通用函数来帮助我们捕捉这些错误。

// to.js
export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}
复制代码

这个通用函数接收一个Promise,然后将处理成功的返回值以数组的形式作为附加值返回,并且在catch方法中接收到捕捉到的第一个错误。

import to from './to.js';

async function asyncTask(cb) {
     let err, user, savedTask;

     [err, user] = await to(UserModel.findById(1));
     if(!user) return cb('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) return cb('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));  
       if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
}
复制代码

上面的例子只是一个使用该解决方案的简单用例,你可以在io.js中添加拦截方法(类似调试的断点),该方法将接收原始错误对象,打印日志或者进行其他任何你想要进行的操作,然后再返回操作后的对象。

我们为这个库创建了一个简单的NPM包(Github Repo),您可以使用以下方法进行安装:

npm i await-to-js
复制代码

这篇文章只是寻找Async/Await功能的一种不同方式,完全基于个人意见。 您可以使用Promise,仅使用try-catch和许多其他解决方案来实现类似的结果。 只要你喜欢并且它适用。

async/await 结合promise使用就好了, 即异步流程控制的最后加上 promise的catch.

async function task(){
    return await req();
}

task().catch(e => console.error(e))
复制代码

结合async/await 结合Promise.all使用时, 如何捕获异常&处理?

async function hello(flag){
    return new Promise((resolve, reject) => {
        if(flag) setTimeout(() => resolve('hello'), 100);
        else reject('hello-error');
    })
}

async function demo(flag){
    return new Promise((resolve, reject) => {
        if(flag) setTimeout(() => resolve('demo'), 100);
        else reject('demo-error');
    })
}

async function main(){
    let res = await hello(1).catch(e => console.error(e));
    console.log('res => ', res);
    let result = await Promise.all([hello(1), demo(1)]);
    // let result = await Promise.all([hello(1), demo(0)]).catch(e => console.error('error => ', e));
    console.log('result => ', result);
}

main()
复制代码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK