ES7 引入 async/await
允许开发人员编写看起来像同步的异步 JavaScript 代码。在当前的 JavaScript 版本中,还可以使用 Promises
,这些功能都是为了简化异步流程并避免回调地狱。
回调地狱是一个术语,用于描述 JavaScript 中的以下情况:
function asyncTasks() { 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); // 更多... }); }); }); }
上述代码使得维护代码和管理控制流变得非常困难。只需考虑一个 if
语句,如果 callbackA
的某些结果等于 foo
,则需要执行其他 async
方法。
拯救 Promises
借助 promises
和 ES6
,可以将之前代码的回调噩梦简化为如下内容:
function asyncTasks(cb) { asyncFuncA .then(AsyncFuncB) .then(AsyncFuncC) .then(AsyncFuncD) .then((data) => cb(null, data)) .catch((err) => cb(err)); }
虽然从代码阅读来看好很多。但在现实世界的场景中,异步流程可能会变得稍微复杂一些,例如在服务器模型 (nodejs
编程)中,可能想要将一个实体数据保存到数据库中,然后根据保存的值查询其他一些实体,如果该值存在,执行其他一些异步任务,在所有任务完成后,可能想要用步骤 1
中创建的对象响应用户。如果在某个步骤中发生了错误,希望通知用户确切的错误。
当然,使用 promises
看起来会比使用普通回调更干净,但它仍然会变得有点混乱。
关于 Promise
需要的了解的:
- Promise.all() 原理解析及使用指南
- Promise.any() 原理解析及使用指南
- Promise.race() 原理解析及使用指南
- Promise.allSettled() 原理解析及使用指南
async/await
在 ECMAScript 2017 中添加了 async 函数和 await 关键字,并在主流脚本库和其他 JavaScript 编程中得到广泛的应用。
这就是 async/await
真正有用的地方,通过它可以编写下面的代码:
async function asyncTasks(cb) { const user = await UserModel.findById(1); if (!user) return cb("用户未找到"); const savedTask = await TaskModel({ userId: user.id, name: "DevPoint" }); if (user.notificationsEnabled) { await NotificationService.sendNotification(user.id, "任务已创建"); } if (savedTask.assignedUser.id !== user.id) { await NotificationService.sendNotification( savedTask.assignedUser.id, "任务已为您创建" ); } cb(null, savedTask); }
上面的代码看起来干净多了,但是错误处理还是存在不足。
进行异步调用时,在执行 promise
期间可能会发生某些事情(如数据库连接错误、数据库模型验证错误等)。由于 async
函数正在等待 Promise
,因此当 Promise
遇到错误时,它会抛出一个异常,该异常将在 Promise
的 catch
方法中被捕获。
在 async/await
函数中,通常使用 try/catch
块来捕获此类错误。
使用 try/catch
后代码如下:
async function asyncTasks(cb) { try { const user = await UserModel.findById(1); if (!user) return cb("用户未找到"); } catch (error) { return cb("程序异常:可能是数据库问题"); } try { const savedTask = await TaskModel({ userId: user.id, name: "DevPoint", }); } catch (error) { return cb("程序异常:任务保存失败"); } if (user.notificationsEnabled) { try { await NotificationService.sendNotification(user.id, "任务已创建"); } catch (error) { return cb("程序异常:sendNotification 失败"); } } if (savedTask.assignedUser.id !== user.id) { try { await NotificationService.sendNotification( savedTask.assignedUser.id, "任务已为您创建" ); } catch (error) { return cb("程序异常:sendNotification 失败"); } } cb(null, savedTask); }
个人对
try/catch
的使用不太喜欢,总觉得这种代码的使用是用于捕获无法预知的错误,比较喜欢 Go 的处理方式,当然这纯属个人观点。
优化 try/catch
在 Go 中的处理方式如下:
data, err := db.Query("SELECT ...") if err != nil { return err }
它比使用 try/catch
块更干净,并且更少地聚集代码,可读和可维护性更高。
但是 await
的问题在于,如果没有为其提供 try/catch
块,它会静默退出函数。除非提供 catch
子句,否则将无法控制它。
利用 await
是在等待 resolve
的 promise
。下面可以制作小的效用函数来捕获这些错误:
function to(promise) { return promise .then((data) => { return [null, data]; }) .catch((err) => [err]); }
效用函数接收一个 promise
,然后将成功响应解析为一个数组,并将返回数据作为第二项,并且从捕获中收到的错误是第一个。
function to(promise) { return promise .then((data) => { return [null, data]; }) .catch((err) => [err]); } async function asyncTask() { let err, user, savedTask; [err, user] = await to(UserModel.findById(1)); if (!user) throw new CustomerError("用户未找到"); [err, savedTask] = await to( TaskModel({ userId: user.id, name: "DevPoint" }) ); if (err) throw new CustomError("程序异常:任务保存失败"); if (user.notificationsEnabled) { const [err] = await to( NotificationService.sendNotification(user.id, "任务已创建") ); if (err) console.error("程序异常"); } }
上面的示例只是该解决方案的一个简单用例,还可以在 to
方法中增加一个拦截器,接收原始错误对象,记录它或在将它传回之前做任何需要做的事情。
总结
本文介绍了 async/await
错误处理的另一种方式,不应该将其视为标准处理方式,因为在很多情况下有一个 try/catch
块就可以了。