[聚合文章] 你真的会在async/await中捕获异常吗?

JavaScript 2018-01-09 25 阅读

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

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。