首页 > 前端开发 > 最新文章

【前端实战】从 try-catch 回调到链式调用:一种更优雅的 async/await 错误处理方案

CSDN博客 2026-05-12 02:44:31 人看过

一、问题背景:async/await 真的解决了一切麻烦吗?

        在 async/await 普及之前,我们的异步代码通常是这样的:

getUserInfo((err, user) => {  if (err) {    showError()    return  }  getUserDetail(user.id, (err, detail) => {    if (err) {      showError()      return    }    render(detail)  }) })

        典型的回调地狱,阅读和维护成本都很高。

        引入 async/await 后,代码变得线性、清晰:

async function loadUser() {  try {    const user = await getUserInfo()    const detail = await getUserDetail(user.id)    render(detail)  } catch (err) {    ElMessage.error('加载失败')  } }

        这已经比回调时代好太多了,但在实际开发中,我遇到了一些问题。

二、真实业务场景下的痛点

1、错误需要“分阶段处理”

        比如一组初始化页面的请求,当然也可以用promise.all()或者promise.allSettled()改写,这里不赘述。

async function initPage() {  try {    const user = await getUserInfo()    const order = await getOrderInfo(user.id)    const coupon = await getCoupon(order.id)    render({ user, order, coupon })  } catch (err) {    ElMessage.error('页面初始化失败')  } }

        但实际需求对不同的错误给出的反馈是不一样的,比如用户信息失败跳登录页,订单失败提示订单异常,优惠券请求失败只给 warning 但不影响主流程等等。

        于是只能写成这样:

async function initPage() {  let user  try {    user = await getUserInfo()  } catch (e) {    redirectToLogin()    return  }  let order  try {    order = await getOrderInfo(user.id)  } catch (e) {    ElMessage.error('订单加载失败')    return  }  let coupon  try {    coupon = await getCoupon(order.id)  } catch (e) {    ElMessage.warning('优惠券加载失败')  }  render({ user, order, coupon }) }

        伴随着 try-catch 被拆散,控制流被不断打断,就带来了新的问题:本质上形成了新的“结构化回调地狱”。层层叠叠的回调函数非常不优雅

2、try-catch 的引入打破了 async/await 的链式范式

        async/await 本来是一种“像同步一样写异步”(即链式调用)的范式,但大量 try-catch 似乎又把链式调用的范式给拉回了回调层面,不做错误处理又不行,做了错误处理又难看。

        同时,大量 try-catch 又导致逻辑分支碎片化,中间变量暴露在外层作用域,进一步降低了可读性,并提升了变量维护的难度。

        采用控制流的方式处理异步请求的错误情况,就一定会出现这种“悖论”,那怎么办呢?


三、借鉴 Go、Rust 语言特性,错误也是一种结果

1、错误优先风格替代 try-catch

        我意识到,在 Go、Rust 这类语言中,错误并不是通过异常抛出,而是通过返回值体现的,例如:

data, err := getUser() if err != nil {  return }

        这就带来了一种实践思路,如果在 JS 中,把 Promise 的成功和失败都“包装成返回值”,不就可以解决上述问题了吗?

2、封装一个 safeAsync 工具函数

        举个例子:

// utils/safeAsync.js export function safeAsync(promise) {  return promise    .then(data => [null, data])   // 成功:[null, data]    .catch(err => [err, null])    // 失败:[err, null] }

        这个函数做的事情很简单,永远 resolve,并把错误“降级”为普通返回值,它的本质就是封装了一个函数用来代替 try-catch ,在多请求依赖场景来体现它的价值。

        那么上述的请求场景就可以变成:

async function initPage() {  const [userErr, user] = await safeAsync(getUserInfo())  if (userErr) {    redirectToLogin()    return  }  const [orderErr, order] = await safeAsync(getOrderInfo(user.id))  if (orderErr) {    ElMessage.error('订单加载失败')    return  }  const [couponErr, coupon] = await safeAsync(getCoupon(order.id))  if (couponErr) {    ElMessage.warning('优惠券不可用')  }  render({    user,    order,    coupon  }) }

        这样就保持了 async/await 的线性结构,错误处理逻辑更加明显更加易读。

四、进阶版 safeAsync 函数设计

        上面已经通过一个基础的 safeAsync 函数解决了回调问题,那 safeAsync 函数能不能有更多设计和可能呢?

        当然可以,我这里给出一种进阶版的设计,各位读者可以根据自己的项目实际情况自由设计和封装自己的 safeAsync 函数:

export async function safeAsync(promise, options = {}) {  const {    silent = false,     // 是否静默失败    toast,              // 错误提示文案    onError             // 自定义错误回调  } = options  try {    const data = await promise    return [null, data]  } catch (err) {    if (!silent && toast) {      ElMessage.error(toast)    }    onError?.(err)    return [err, null]  } }

        在这个进阶 safeAsync 函数中,除了接受请求返回的 promise 对象外,还收集一个配置项参数 options ,options 中可以传入是否需要弹窗提醒用户,弹窗提示的文案是什么以及自定义错误的回调函数,实现更自由的错误处理。


五、结语


        虽然 async/await 解决了传统回调地狱问题,但 try-catch 可能制造新的结构复杂度,导致代码可读性下降,通过合适的封装和抽象设计,能够大大提升多请求依赖、分阶段错误处理场景下的代码质量。

        只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

        JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、DOM操作等

版权声明:倡导尊重与保护知识产权。未经许可,任何人不得复制、转载、或以其他方式使用本站《原创》内容,违者将追究其法律责任。本站文章内容,部分图片来源于网络,如有侵权,请联系我们修改或者删除处理。

编辑推荐

热门文章