95

你真的会在async/await中捕获异常吗?

 6 years ago
source link: https://segmentfault.com/a/1190000012767617?amp%3Butm_medium=referral
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.

你真的会在async/await中捕获异常吗?

原文链接:Catching without Awaiting

当执行一项需要等待一段时间才能返回的任务时,如果使用async/await,就显得比较麻烦了。如果async方法还没有得到返回值,我们就捕获不到其中的异常。

在我的上一篇文章Learn to Throw Again中写到,当使用async/await时,如何同时捕获到回调函数和throw抛出的错误。在这篇文章中,我们将讨论如何在“后台”中执行异步操作并捕获异常(这里使用双引号,因为在单线程平台上没有真正的后台操作)

从回调函数的模式开始,思考下列代码:

function email(user, message, callback) {
  if (!user) {
    // 抛出异常
    throw new Error('Invlid user');
  }
  if (!user.address) {
    // 回调函数,可能抛出异常
    return callback();
  }
  // 异步的
  return mailer.send(user.address, message, callback);
}

上述代码遵循典型的throw-on-bad-input / callback-asynchronous-errors模式(一旦程序接收到错误的输入,异步抛出异常),如果我们想要发出一封邮件,我们这样调用:

email(user, message, () => {});

对于非法的输入,调用这个函数依旧可能抛出异常。但是,如果电子邮件在传输中产生错误,这个函数调用时会忽略异步抛出的错误。

我们把它改为Promise的版本:

function email(user, message) {
  if (!user) {
    throw new Error('Invlid user');
  }
  if (!user.address) {
    return Promise.resolve();
  }
  return mailer.send(user.address, message); // 函数返回一个Promise
}

这样,对于非法的输入,依旧可以捕获到异常。而对于mailer.send()操作则会返回一个Promise,我们能够轻松地通过Promise.catch()捕获到异常:

email(user, message).catch(() => {});

不管是回调函数还是Promise,他们都是异步的,我们的应用程序都不会因为email发送而被阻塞。

对于async/await的模式,如果在try...catch语句中不使用await关键字,那么try...catch子句不会真正工作。来看下面的async版本:

function email(user, message) {
  if (!user) {
    throw new Error('Invlid user');
  }
  if (!user.address) {
    return;
  }
  return mailer.send(user.address, message); // async function
}

如果我们像这样去调用:

try {
  email(user, message);
} catch (err) {
  Bounce.rethrow(err, 'system');
}

对于非法的输入错误,仍然会正常地抛出异常,这没问题。但是对于任何异步返回的异常,例如在mailer.send()抛出的异常,则会被忽略掉。不管这种错误我们想不想捕获到,反正都是捕获不到的。为了修补这个bug,则要使用await关键字。但是问题来了,这将会导致整个“后台操作”的阻塞。

有一种方案是混用async/awaitPromise

email(user, message).catch(() => {});

但这样的问题在于,对于没有address的用户,这个方法返回的返回值类型并不是Promise,因而其也不会有catch()方法,因此程序会出现TypeError: Cannot read property ‘catch’ of undefined这样的错误。

你可能会尝试直接把email()函数声明为async函数, 并使得它一定会返回一个Promise,但是这并不是一个很好的解决方案,因为async / await其实也只是Promise对象的一层包装。如果不使用await关键字,把一个函数声明为async函数是完全没有必要的。因为async函数总是要通过返回一个Promise,通过next-tick拿到结果,这样会浪费Promise包装和next-tick事件循环机制所造成的性能损耗。

此外,如果要在循环中使用async函数,并且这个循环中执行了很多任务,但是其实很多任务并不是真正意义上异步的,那就没有必要使用async / await,可以参考hapi.js中的checking if you really need to await下列代码判断是否真的需要使用await,这样或许能获得一些性能的提升:

var response = (typeof func === 'function' ? func(this) : this._invoke(func));
if (response && typeof response.then === 'function') { // Skip await if no reason to
  response = await response;
}

判断是否真的需要await,其实就是判断其是否存在then方法,并且then方法是一个函数。因为await的作用其实就是取得一个异步操作的返回结果。

如果你能够保证email方法总是返回一个Promise,我们可以通过更改我们的email()函数来达到这一点,但这样就显得急功近利了!代码显得十分不简洁,而且使用了很不必要的异步操作。在一个完整的async/await函数调用栈中,不需要我们手动构建Promise。对于这个例子来说还好,更重要的是,我们不可能总通过改变email()方法来实现,因为这只是一个例子,在实际运用中,可能email()方法是通过模块引入的。

其中一种解决方案是通过await关键字来调用async函数。通常情况下,在一个函数中使用阻塞操作,如果不等待这个函数执行完成,它不会抛出异常,但是我们可以通过try...catch来包裹:

async function backgroundEmail(user, message) {
  try {
    await email(user, message);
  } catch (err) {
    Bounce.rethrow(err, 'system');
  }
}

然后不通过await调用backgroundEmail

backgroundEmail(user, message);

这样我们不但能够捕获到应用程序的异常,还能够捕获到异步抛出的异常。

为了让异常捕获更加简单,我们使用Bounce模块,它提供了一个background()方法。

Bounce.background(() => email(user, message));

如果我们使用Node.jsAssertionError原型,这样就能够使得Bounce抛出输入异常的错误了。

async/await函数去除了一些同步函数(() => {})的功能,为了达到和普通函数相同的效果,我们不得不写一些额外的代码来实现。但是使用新的工具库,可以很简便地突破这一限制。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK