从不用try-catch实现的async/await语法来说说错误处理

开发 前端
前不久看到 Dima Grossman 写的 How to write async await without try-catch blocks in Javascript。看到标题的时候,我感到非常好奇。我知道虽然在异步程序中可以不使用 try-catch 配合 async/await 来处理错误,但是处理方式并不能与 async/await 配合得很好,所以很想知道到底有什么办法会比 try-catch 更好用。

[[208471]]

前不久看到 Dima Grossman 写的 How to write async await without try-catch blocks in Javascript。看到标题的时候,我感到非常好奇。我知道虽然在异步程序中可以不使用 try-catch 配合 async/await 来处理错误,但是处理方式并不能与 async/await 配合得很好,所以很想知道到底有什么办法会比 try-catch 更好用。

Dima 去除 try-catch 的方法

当然套路依旧,Dima 讲到了回调地狱,Promise 链并最终引出了 async/await。而在处理错误的时候,他并不喜欢 try-catch 的方式,所以写了一个 to(promise) 来对 Promise 进行封装,辅以解构语法,实现了同步写法但类似 Node 错误标准的代码。摘抄代码如下

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

应用示例:

  1. import to from "./to.js"
  2.  
  3. async function asyncTask(cb) { 
  4.     let err, user, savedTask; 
  5.  
  6.     [err, user] = await to(UserModel.findById(1)); 
  7.     if (!userreturn cb("No user found"); 
  8.  
  9.     [err, savedTask] = await to(TaskModel({ userId: user.id, name"Demo Task" })); 
  10.     if (err) return cb("Error occurred while saving task"); 
  11.  
  12.     if (user.notificationsEnabled) { 
  13.         const [err] = await to(NotificationService.sendNotification(user.id, "Task Created")); 
  14.         if (err) return cb("Error while sending notification"); 
  15.     } 
  16.  
  17.     cb(null, savedTask); 
  18.  

Dima 的办法让人产生的了熟悉的感觉,Node 的回调中不是经常都这样写吗?

  1. (err, data) => { 
  2.     if (err) { 
  3.         // deal with error 
  4.     } else { 
  5.         // deal with data 
  6.     } 
  7.  

所以这个方法真的很有意思。不过回过头来想一想,这段代码中每当遇到错误,都是将错误消息通过 cb() 调用推出去,同时中断后续过程。像这种中断式的错误处理,其实正适合采用 try-catch。

使用 try-catch 改写上面的代码

要用 try-catch 改写上面的代码,首先要去掉 to() 封装。这样,一旦发生错误,需要使用 Promise.prototype.catch() 进行捕捉,或者使用 try-catch 对 await promise 语句进行捕捉。捕捉到的,当然是每个业务代码里 reject 出来的 err。

然而注意,上面的代码中并没有直接使用 err,而是使用了自定义的错误消息。所以需要对 reject 出来的 err 进一步处理成指定的错误消息。当然这难不到谁,比如

  1. someAsync().catch(err => Project.reject("specified message")); 

然后再最外层加上 try-catch 就好。所以改写之后的代码是:

  1. async function asyncTask(cb) { 
  2.     try { 
  3.         const user = await UserModel.findById(1) 
  4.             .catch(err => Promise.reject("No user found")); 
  5.  
  6.         const savedTask = await TaskModel({ userId: user.id, name"Demo Task" }) 
  7.             .catch(err => Promise.reject("Error occurred while saving task")); 
  8.  
  9.         if (user.notificationsEnabled) { 
  10.             await NotificationService.sendNotification(user.id, "Task Created"
  11.                 .catch(err => Promise.reject("Error while sending notification")); 
  12.         } 
  13.  
  14.         cb(null, savedTask); 
  15.     } catch (err) { 
  16.         cb(err); 
  17.     } 
  18.  

上面这段代码,从代码量上来说,并没有比 Dima 的代码减少了多少工作量,只是去掉了大量 if (err) {} 结构。不习惯使用 try-catch 的程序员找找不到中断点,但习惯了 try-catch 的程序员都知道,业务过程中一旦发生错误(异步代码里指 reject),代码就会跳到 catch 块去处理 reject 出来的值。

但是,一般业务代码 reject 出来的信息通常都是有用的。假如上面的每个业务 reject 出来的 err 本身就是错误消息,那么,用 Dima 的模式,仍然需要写

  1. if (err) return cb(err); 

而用 try-catch 的模式,就简单多了

  1. async function asyncTask(cb) { 
  2.     try { 
  3.         const user = await UserModel.findById(1); 
  4.         const savedTask = await TaskModel({ userId: user.id, name"Demo Task" }); 
  5.  
  6.         if (user.notificationsEnabled) { 
  7.             await NotificationService.sendNotification(user.id, "Task Created"); 
  8.         } 
  9.  
  10.         cb(null, savedTask); 
  11.     } catch (err) { 
  12.         cb(err); 
  13.     } 
  14.  

为什么?因为在 Dima 的模式中,if (err) 实际上处理了两个业务:一是捕捉会引起中断的 err ,并将其转换为错误消息,二是通过 return 中断业务过程。所以当 err 转换为错误消息这一过程不再需要的时候,这种捕捉中断再重新引起中断的处理主显得多余了。

继续改进

用函数表达式改善 try-catch 逻辑

当然还有改进的空间,比如 try {} 块中的代码比较长,会造成阅读不太方便,try-catch 的逻辑有被“切断”的感觉。这种情况下可以使用函数表达式来改善

  1. async function asyncTask(cb) { 
  2.     async function process() { 
  3.         const user = await UserModel.findById(1); 
  4.         const savedTask = await TaskModel({ userId: user.id, name"Demo Task" }); 
  5.  
  6.         if (user.notificationsEnabled) { 
  7.             await NotificationService.sendNotification(user.id, "Task Created"); 
  8.         } 
  9.         return savedTask; 
  10.     } 
  11.  
  12.     try { 
  13.         cb(null, await process()); 
  14.     } catch (err) { 
  15.         cb(err); 
  16.     } 
  17.  

如果对错误的处理代码比较长,也可以写成单独的函数表达式。

如果过程中每一步的错误处理逻辑不同怎么办

如果发生错误,不再转换为错误消息,而是特定的错误处理逻辑,怎么办?

思考一下,我们用字符串来表示错误消息,以后可以通过 console.log() 来处理处理。而逻辑,最适合的表示当然是函数表达式,最终可以通过调用来进行统一处理

  1. async function asyncTask(cb) { 
  2.     async function process() { 
  3.         const user = await UserModel.findById(1) 
  4.             .catch(err => Promise.reject(() => { 
  5.                 // deal with error on looking for the user 
  6.                 return "No user found"
  7.             })); 
  8.  
  9.         const savedTask = await TaskModel({ userId: user.id, name"Demo Task" }) 
  10.             .catch(err => Promise.reject(() => { 
  11.                 // making model error 
  12.                 // deal with it 
  13.                 return err === 1 
  14.                     ? "Error occurred while saving task" 
  15.                     : "Error occurred while making model"
  16.             })); 
  17.  
  18.         if (user.notificationsEnabled) { 
  19.             await NotificationService.sendNotification(user.id, "Task Created"
  20.                 .catch(err => Promise.reject(() => { 
  21.                     // just print a message 
  22.                     logger.log(err); 
  23.                     return "Error while sending notification"
  24.                 })); 
  25.         } 
  26.  
  27.         return savedTask; 
  28.     } 
  29.  
  30.     try { 
  31.         cb(null, await process()); 
  32.     } catch (func) { 
  33.         cb(func()); 
  34.     } 
  35.  

甚至还可以处理更复杂的情况

现在应该都知道 .catch(err => Promise.reject(xx)),这里的 xx 就是 try-catch 的 catch 块捕捉到的对象,所以如果不同的业务 reject 出来不同的对象,比如有些是函数(表示错误处理逻辑),有些是字符串(表示错误消息),有些是数字(表示错误代码)——其实只需要改 catch 块就行

  1. try { 
  2.        // ...    
  3.    } catch(something) { 
  4.        switch (typeof something) { 
  5.            case "string"
  6.                // show message something 
  7.                break; 
  8.            case "function"
  9.                something(); 
  10.                break; 
  11.            case "number"
  12.                // look up something as code 
  13.                // and show correlative message 
  14.                break; 
  15.            default
  16.                // deal with unknown error 
  17.        } 
  18.    }  

小结

我没有批判 Dima 的错误处理方式,这个错误处理方式很好,很符合 Node 错误处理的风格,也一定会受到很多人的喜爱。由于 Dima 的错误处理方式给带灵感,同时也让我再次审视了一直比较喜欢的 try-catch 方式。

用什么方式取决于适用场景、团队约定和个人喜好等多种因素,在不同的情况下需要采用不同的处理方式,并不是说哪一种就一定好于另一种——合适的才是***的! 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2024-03-05 18:15:28

AsyncAwait前端

2020-09-27 07:48:40

不用try catch

2009-07-21 14:30:38

Scalatry-catch

2021-06-28 07:27:43

AwaitAsync语法

2021-06-15 05:36:45

Gulpawaitasync

2020-10-14 12:10:22

Javatry-catch代码

2020-05-29 08:14:49

代码Try-Catch程序员

2022-02-10 09:04:50

架构

2024-04-08 00:00:00

asyncawaiPromise

2020-08-24 13:35:59

trycatchJava

2023-09-07 07:53:21

JavaScriptGoRust

2022-10-24 08:55:13

Go工具链开发者

2021-04-14 07:08:14

Nodejs错误处理

2021-08-18 07:05:57

ES6Asyncawait

2024-03-27 08:18:02

Spring映射HTML

2014-07-15 10:31:07

asyncawait

2021-04-29 09:02:44

语言Go 处理

2014-11-17 10:05:12

Go语言

2016-11-22 11:08:34

asyncjavascript

2023-11-13 17:01:26

C++编程
点赞
收藏

51CTO技术栈公众号