如何处理proimise、await产生的错误

前端 潘老师 5个月前 (11-29) 109 ℃ (0) 扫码查看

本文主要讲解关于如何处理proimise、await产生的错误相关内容,让我们来一起学习下吧!

如何正确处理proimise、await产生的错误

最近在一些小童鞋的群上,讨论了一些很普遍的错误处理的话题。发现大部分初级甚至中级前端,都不知道如何系统的处理一个错误。普遍出现到处try…catch的问题。甚至有些公知,还推出各种奇怪的库来处理异步问题。
本文就以一个很常见的例子,说明我是如何处理错误的。

假设一个场景:

  • 根据token获取用户信息
  • 根据用户信息的 .vipRank vip等级推送广告
  • 上述每个过程都需要请求api

我们有两个实体

interface User {
  name: string
  vipRank: number
  // ... 省略
}
interface Ad {
  // ... 省略
}

我们很自然会相当把这个过程三个函数

async function getUserInfo() {
  return (await axios.get('user/get-info')).data;
}

async function getUserAds(rank: number) {
  return (await axios.get('user/recommand-ads'), { rank }).data;
}

async function main() {
  const info = await getUserInfo();
  const ads = await getUserAds(info.vipRank);
  console.log(ads);
}

// 如果你看不懂async函数,以下代码是等价的

function getUserInfo() {
  // 注意,必须return才是等价的,很多初级程序不返回,高级程序员(包括我)经常粗心漏了
  return axios.get('user/get-info').then(({data}) => data);
}

function getUserAds(rank: number) {
  // 记得return
  return axios.get('user/get-info', { rank }).then(({data}) => data);
}

function main() {
  // 记得return
  return getUserInfo().then((info) => {
    const ads = getAds(info.vipRank);
    console.log(ads);
  });
}

那么问题来了,我们应该在什么地方进行捕获错误

这个问题我们先放以下,我们先剖析以下在不同地方捕获,会出现什么情况

假如在 getUserInfogetUserAds 捕获


async function getUserInfo() {
  try {
    return (await axios.get('user/get-info')).data;
  } catch(e) {
    console.error(e);
  }
}

async function getUserAds(rank: number) {
  try {
    return (await axios.get('user/recommand-ads'), { rank }).data;
  } catch(e) {
    console.error(e);
  }
}

// 同样,promise的例子

function getUserInfo() {
  // 再次强调,必须return才是等价的
  return axios.get('user/get-info')
    .then(({data}) => data)
    .catch((err) => {
      console.error(e);
    });
}

// getUserAds 我就不写了,自己脑补吧

在不改main函数的情况,如果一切请求正常的情况,用户会拿到 Ad 实体的数组

但是如果用户在如下步骤请求错误了:

getUserInfo 出现错误

假设用户token异常,或者token过期,服务的返回 401 Unauthorized

// 下面代码不再提供promise,均以await演示


async function getUserInfo() {
  try {
    return (await axios.get('user/get-info')).data;
  } catch(e) {
    console.error(e);
    // ⬇ 其实这里相当于少了一句
    // return undefined;
  }
}

async function main() {
  const info = await getUserInfo();
  // ↑ 由于 info 是 undefind
  // 我们这里会收获一个 
  // Uncaught TypeError: Cannot read properties of undefined (reading 'vipRank')
  // 并且导致程序被中断


  // 程序到达不了这个位置
  const ads = await getUserAds(info.vipRank);
  console.log(ads);
}

可以看出,我们意外收获了一个

Uncaught TypeError: Cannot read properties of undefined (reading 'vipRank')

这不是我们想要的,也是我经常说的 “错误转移”,什么是错误转移,就是原本不是这一个错误
但是由于错误的捕获,导致其他错误冒出来

当然,有些同学说,我们可以继续增加try,避免意外的错误


async function main() {
  try {
    const info = await getUserInfo();
    const ads = await getUserAds(info.vipRank);
    console.log(ads);
  } catch(e) {
    console.error(e);
  }
}

但是这样倒推下去会存在三个问题

  • 假设我们的main函数不是程序入口,我们需要一直添加try,直到到真正的入口为止
  • 会显示多个console.error,取决于我们catch多少次,而且很多错误根本的显示不是预期的
  • 如果调用栈十分深,我们根本不知道原本的错误是什么

正确做法,Let it throw

正确的写法,其实一开始已经是对的了,就是都不catch

// 下面代码不再提供promise,均以await演示


async function getUserInfo() {
  // 为了方便理解,我把代码拆成两行
  const res = await axios.get('user/get-info');
  // ↑ 如果上述逻辑错误,自动抛出,程序会被中断,往后的所有代码均不会被执行

  // 程序到达不了这个位置
  return res.data;
}

async function main() {
  const info = await getUserInfo();
  // ↑ 由于程序已经中断,后续代码根本不会执行

  // 程序到达不了这个位置
  const ads = await getUserAds(info.vipRank);
  console.log(ads);
}

我们会获得一个由于请求错误导致的 401 Unauthorized 错误
这个错误,才是我们需要的,而且不会出现上述问题

那么有些同学问,那么我应该怎么让用户知道用户没登陆导致错误:

我们应该利用一些系统钩子,或者框架,库给我提供的生命周期函数来处理


// 错误处理器
function errHandler(err: unknown) {
  console.error(error);
  alert(error); // 当然,你可以用ui库提供的toast,或者message等
}

// 所有没有被catch的promise都会落入这个事件
window.addEventListener('unhandledrejection', (evt) => {
  const error = evt.reason;
  
  // 可以将错误转发到error
  window.reportError(error);
  
  // 交给错误处理器处理
  errHandler(error)
});


window.addEventListener('error', (error) => {
  // 交给错误处理器处理
  errHandler(error)
})

  • unhandledrejection 事件
  • error 事件
  • reportError api

当然,如果我们浏览器版本比较低,不支持部分api,我们可以借助框架提供的一些钩子来实现
以下以vue技术栈为例,我们可以使用:

  • Vue errorHandler
  • Vue Router onError

总体原则是,

我们应该在最贴近堆栈顶层进行处理

错误处理器

这里引入错误处理器的概念
一般来说一个成熟的错误处理器,包含四个以上步骤

1)转换错误,因为错误类型其实是unknown的,你永远不知道一个错误是什么东西。可能是字符串,可是Error对象,可是undefined。另外,如果遇到一些流程上认为的错误,但是系统实际没有错误的情况,
比如 new DOMException('user abort', 'AbortError')就直接return,不继续后续的错误流程

2)log错误,一般是转换错误后,将错误打印到控制台,方便开发人员查看。当然还要涉及到怎么打印方便,比如我们打印webgl错误的时候,经常会伴随shader错误。那么我需要有一段更好的打印方法来显示具体哪行代码错误

function getScriptErrorLinesLogMessage(
  script: string, errorLine: number, showLines = 5
) {
  const lines = script.split('n');
  const lineIdx = errorLine - 1;
  // 标记控制台的 字符串 css 注入点标记 %c,注意避开前后的空白字符
  lines[lineIdx] = lines[lineIdx].replace(/(^s*)/, '$1%c').replace(/(s*$)/, '%c$1');

  const start = Math.max(0, errorLine - showLines);
  const end = Math.min(lines.length, errorLine + showLines);

  return lines.slice(start, end).map((line, idx) => {
    const lineNumber = idx + start + 1;
    return `${lineNumber === errorLine ? '➡️t' : 't'}${lineNumber}t${line}`;
  }).join('n');
}

3)上报错误:交给sentry或者一些日志系统进行上报,自动报障。当然,后续还有很多流程。比如根据内网sourcemap分析错误堆栈,自动录入错误工单系统,对接issue tracker等

4)安抚用户:说白了最后一步可能就是得让用户知道到底什么情况了。比如 401 Unauthorized ,应该弹出登录框,或者转跳登录页。如果不是401或者其他特殊的错误,如果系统又国际化,应该在这个步骤进行文案的

修饰错误或者业务上的捕获错误

有些场景下,我们是需要修饰或者捕获业务上的错误

假如我增加一些场景的

  • 根据token获取用户信息
  • (+) 如果401,则代表用户未登录
  • 根据用户信息的 .vipRank vip等级推送广告
  • (+) 未登录用户需要额外的api获取默认推荐广告
  • 上述每个过程都需要请求api

正确的做法,我们就可以大方的使用 catch,让流程落到正确的情况,因为

对于当前场景来说,用户没有登录也是一种正常情况

但是要注意的是,我们应该只处理 “没有登录” 这种错误,
而其他错误,我们应该继续抛出,因为我们永远不知道,运行时
我们的错误,是因为没有登录,还是因为我们写错代码导致的,或者其他情况
如果是后者,我们应该遵循 let it throw 原则,对不可控的错误,进行抛出

// rank 改为非必填
function getUserAds(rank?: number) {
  return (await axios.get('user/recommand-ads'), { rank }).data
}


function main() {
  let info: User | null;
  try {
    info = await getUserInfo();
  } catch(err) {
    // 这里我们判断十分严谨,因为 **你永远不知道一个错误是什么东西**
    // 我们可以封装这些判断,用于区分具体的错误
    if (
      typeof err !== 'object' || !err ||
      typeof err.response !== 'object' || !err.response ||
      err.response.status !== 400
    ) {
      // 我们处理不了的东西,不要瞎处理
      throw err;  
    }
    info = null;
  }
  // 我们不一定又info
  const ads = await getUserAds(info?.vipRank);
  console.log(ads);

}

以上就是关于如何处理proimise、await产生的错误相关的全部内容,希望对你有帮助。欢迎持续关注潘子夜个人博客(www.panziye.com),学习愉快哦!


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/11968.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】