摘要:async/await 是 ES7 中引入的语法糖,它彻底改变了 JavaScript 中异步编程的方式。它让我们能够以一种看似同步的方式编写异步代码,极大地提高了代码的可读性和可维护性。
async/await 是 ES7 中引入的语法糖,它彻底改变了 JavaScript 中异步编程的方式。它让我们能够以一种看似同步的方式编写异步代码,极大地提高了代码的可读性和可维护性。
然而,凡事皆有两面。当我们享受 async/await 带来的便利时,一个“老朋友”却如影随形,那就是 try...catch。
为了捕获 await 后面 Promise 的 reject 状态,我们必须将代码包裹在 try...catch 块中。让我们来看一个典型的例子,比如从服务器获取用户信息:
import { fetchUserById } from './api';async function displayUser(userId) { try { const user = await fetchUserById(userId); console.log('用户信息:', user.name); // ... 更多基于 user 的操作 } catch (error) { console.error('获取用户失败:', error); // ... 相应的错误处理逻辑,比如显示一个提示 }}这段代码本身没有问题,它能正常工作。但问题在于,如果你的业务逻辑稍微复杂一点,比如需要连续请求多个接口,代码就会变成这样:
async function loadPageData(userId) { try { const user = await fetchUserById(userId); console.log('用户信息:', user.name); try { const posts = await fetchPostsByUserId(user.id); console.log('用户文章:', posts); try { const comments = await fetchCommentsForPosts(posts[0].id); console.log('文章评论:', comments); } catch (commentError) { console.error('获取评论失败:', commentError); } } catch (postError) { console.error('获取文章失败:', postError); } } catch (userError) { console.error('获取用户失败:', userError); }}看到这些层层嵌套的 try...catch,你是否感到了一丝窒息?这种写法存在几个明显的问题:
代码冗余:每个异步操作都需要重复的 try...catch 结构,增加了大量样板代码。可读性差:核心的“快乐路径”(Happy Path)代码被包裹在 try 块中,增加了缩进层次,干扰了正常的阅读流。关注点混合:成功逻辑和失败逻辑紧密地耦合在同一个代码块里,使得函数职责不够单一。那么,有没有一种方法可以摆脱这种困境呢?答案是肯定的。
我们可以借鉴 Go 语言的错误处理模式。在 Go 中,函数通常会返回两个值:result 和 error。调用者通过检查 error 是否为 nil 来判断操作是否成功。
我们可以将这种思想引入到 JavaScript 的 async/await 中。创建一个辅助函数(我们称之为 to),它接收一个 Promise作为参数,并且永远不会被 reject。相反,它总是 resolve 一个数组,格式为 [error, data]。
如果 Promise 成功 resolve,它返回 [null, data]。如果 Promise 失败 reject,它返回 [error, null]。让我们来实现这个 to 辅助函数。
// utils/promise.ts/** * @description 接收一个 Promise,并返回一个元组 [error, data] * @param {Promise} promise - 要处理的 Promise * @returns {Promise} */export function to(promise: Promise): Promise { return promise .then((data: T) => [null, data]) .catch((err: Error) => [err, undefined]);}如果你不使用 TypeScript,纯 JavaScript 版本如下:
// utils/promise.jsexport function to(promise) { return promise .then(data => [null, data]) .catch(err => [err, undefined]);}这个 to 函数非常小巧,但威力巨大。它将 try...catch 的逻辑封装在了内部,向我们暴露了一个统一、扁平的接口。
现在,让我们用新的 to 函数来重构之前的 displayUser 函数:
import { fetchUserById } from './api';import { to } from './utils/promise';async function displayUser(userId) { const [error, user] = await to(fetchUserById(userId)); if (error || !user) { console.error('获取用户失败:', error); // ... 相应的错误处理逻辑 return; } // 到这里,代码的"快乐路径"是清晰且扁平的 console.log('用户信息:', user.name); // ... 更多基于 user 的操作}看看发生了什么变化:
没有 try...catch 了! 整个函数体变得非常扁平。错误优先处理:我们首先通过一个 if 语句检查并处理错误(这被称为“卫语句”或 Guard Clause),然后提前返回。可读性极高:处理完错误后,剩下的代码都是成功路径下的核心逻辑,一目了然,不再有任何嵌套。现在,我们再来挑战那个恐怖的嵌套地狱 loadPageData:
import { to } from './utils/promise';// ... import APIsasync function loadPageData(userId) { const [userError, user] = await to(fetchUserById(userId)); if (userError || !user) { return console.error('获取用户失败:', userError); } console.log('用户信息:', user.name); const [postsError, posts] = await to(fetchPostsByUserId(user.id)); if (postsError || !posts) { return console.error('获取文章失败:', postsError); } console.log('用户文章:', posts); const [commentsError, comments] = await to(fetchCommentsForPosts(posts[0].id)); if (commentsError || !comments) { return console.error('获取评论失败:', commentsError); } console.log('文章评论:', comments);}简直是天壤之别!代码变成了线性的、可预测的流程,每个步骤的错误处理都清晰独立。
async function loadDashboard(userId) { const [ [userError, userData], [settingsError, settingsData] ] = await Promise.all([ to(fetchUser(userId)), to(fetchUserSettings(userId)) ]); if (userError) { console.error('加载用户数据失败'); // 处理用户错误 } if (settingsError) { console.error('加载用户设置失败'); // 处理设置错误 } // 即使其中一个失败,另一个成功的数据依然可用 if (userData) { // ... } if (settingsData) { // ... }}使用 Promise.all 配合 to 函数,你可以优雅地处理多个 Promise 并发执行时部分成功、部分失败的场景,而传统的 try...catch 会在任何一个 Promise 失败时直接进入 catch 块,导致所有结果丢失。
try...catch 是 JavaScript 错误处理的基石,我们并非要完全消灭它。实际上,我们的 to 函数内部就使用了它。关键在于,我们应该将它抽象和封装起来,而不是在业务代码中一次又一次地手动编写。
来源:不秃头程序员