原文:How to write async await without try-catch blocks in Javascript 链接:https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/ 翻译:Robin

ES7 的 Async/await 允许开发者书写“看似”同步的异步 JS 代码。现有的 JS 版本中引入了 Promise,使得异步流进一步简化并避免了回调噩梦。

回调噩梦描述了 JS 中下述的场景:

  1. function AsyncTask() {
  2. asyncFuncA(function(err, resultA){
  3. if(err) return cb(err);
  4. asyncFuncB(function(err, resultB){
  5. if(err) return cb(err);
  6. asyncFuncC(function(err, resultC){
  7. if(err) return cb(err);
  8. // And so it goes....
  9. });
  10. });
  11. });
  12. }

这使得代码难以维护,控制流管理变得困难。想象一下,如果 A 的回调返回的结果为 ‘foo’,这时需要去执行别的异步方法。

拯救者 Promises

随着 promises 和 ES6 的到来,我们可以将上述噩梦代码改成这样:

  1. function asyncTask(cb) {
  2. asyncFuncA.then(AsyncFuncB)
  3. .then(AsyncFuncC)
  4. .then(AsyncFuncD)
  5. .then(data => cb(null, data)
  6. .catch(err => cb(err));
  7. }

看起来是不是很棒?

但是现实场景中异步流可能会变得更加复杂,比如你想在服务器模型中存储一个实体到数据库中,然后基于存储值进行实体查询,如果存在,然后进行其他的异步任务,当所有任务结束后返回给用户一个第一步中可用的对象。在任何步骤中发生错误时将错误通知到用户。

使用 promises 当然会看起来很清爽,后接一些带有回调的 then 语句,但是恕我直言,这仍然有一些凌乱。

ES7 Async/await

注意:虽然 async/await 一时爽,但是你仍然需要转换器,使用 babel 或者 typescript 来进行 polyfills。

我发现 async/await 十分有用的地方在于你可以写类似这样的代码:

  1. async function asyncTask(cb) {
  2. const user = await UserModel.findById(1);
  3. if(!user) return cb('No user found');
  4. const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
  5. if(user.notificationsEnabled) {
  6. await NotificationService.sendNotification(user.id, 'Task Created');
  7. }
  8. if(savedTask.assignedUser.id !== user.id) {
  9. await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
  10. }
  11. cb(null, savedTask);
  12. }

代码看起来更清爽了,不过,错误处理怎么办?

异步调用时在 promise 执行期间会发生任何事(数据库链接错误,数据模型验证错误等等)。

因为异步函数一直等待 Promises,当一个 promise 遇到错误它会抛出一个异常然后在 promise 外部的 catch 方法中进行捕获。

在 async/await 函数中通常会使用 try/catch 块来捕获类似错误。

我没有类型语言的背景,因此,try/catch 在我看来不是那么清爽。当然,这只是个人偏好。

那么,上述代码将会变成这样:

  1. async function asyncTask(cb) {
  2. try {
  3. const user = await UserModel.findById(1);
  4. if(!user) return cb('No user found');
  5. } catch(e) {
  6. return cb('Unexpected error occurred');
  7. }
  8. try {
  9. const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
  10. } catch(e) {
  11. return cb('Error occurred while saving task');
  12. }
  13. if(user.notificationsEnabled) {
  14. try {
  15. await NotificationService.sendNotification(user.id, 'Task Created');
  16. } catch(e) {
  17. return cb('Error while sending notification');
  18. }
  19. }
  20. if(savedTask.assignedUser.id !== user.id) {
  21. try {
  22. await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
  23. } catch(e) {
  24. return cb('Error while sending notification');
  25. }
  26. }
  27. cb(null, savedTask);
  28. }

另辟蹊径

最近,我在使用 go-lang 进行搬砖,我非常喜欢这样的解决方式:

  1. data, err := db.Query("SELECT ...")
  2. if err != nil { return err }

我认为比 try-catch 块更加清爽,并且代码量更少,这增加了可读性和可维护性。

但是即便你的函数中没有 try-catch 块, await 的问题仍然存在。除非提供 catch 语句否则你没有办法进行错误控制。

我和我的好友 Tomer Barnea 一起尝试找寻一种清爽的方式并最终找到了下面的方法:

还记得 await 是在等待 promise 的 resolve 吗?

我们可以使用一个简单的工具函数来帮助我们捕获这些错误:

  1. // to.js
  2. export default function to(promise) {
  3. return promise.then(data => {
  4. return [null, data];
  5. })
  6. .catch(err => [err]);
  7. }

工具函数接受一个 promise,then 接受一个成功的响应并作为 return 返回值数组的第二个数据。catch 获取错误并作为 return 返回值数组的第一个数据。

然后 async 代码改成这样:

  1. import to from './to.js';
  2. async function asyncTask() {
  3. let err, user, savedTask;
  4. [err, user] = await to(UserModel.findById(1));
  5. if(!user) throw new CustomerError('No user found');
  6. [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
  7. if(err) throw new CustomError('Error occurred while saving task');
  8. if(user.notificationsEnabled) {
  9. const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
  10. if (err) console.error('Just log the error and continue flow');
  11. }
  12. }

上述代码仅仅是一个简单的解决方式,你可以在 to.js 方法内部加入拦截器来接受一个原始的错误对象,在返回前打印出来或者做别的事情。

本文探究了另一种聚焦 async/await 错误处理的方式。它不能看作是 async/await 函数的 goto 语句,并且在大部分情况中,一个顶部的 catch 更加适用。有时,我们不想暴露模型实现的错误对象,转而为底层错误实现提供一个定制化的错误对象。

我们提供了一个简单的 NPM 包,你可以安装使用:

Github 仓库地址

  1. npm i await-to-js