你真的会处理TS中的Error么

4,876 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

这篇文章来自我们团队的啸达,好看的皮囊千篇一律,有趣的灵魂万里挑一。而他的文章,和他本人一样有趣~

前言

不管是平时开发、问题修复、性能优化…一个程序员的日常就是不断地生产和消灭Bug,最后与异常们"和解"的过程。处理异常已经成为程序员的家常便饭了,尤其是运行时异常,我们经常通过异常打印的方式先了解一下异常的信息,再通过异常反推出业务逻辑中的问题,最后增加对应的异常的捕获处理。

所以,异常本身很常见,定位手段很多样,异常的处理方式也很清晰,那为什么需要单独拿出来说说呢?这是因为本人在项目发展过程中,先经历了从JS转TS的过程,随着TS版本的更迭又经历一系列的代码调整和适配的动作。其实每一次调整都是语言不断进步的体现,在被动接受调整和适配的同时,我们也能顺便扒一扒语言升级过程中的“小动作”。看看TS是怎么通过一个“无形的小爪子”控制我们的编程方式的...

文中提到的案例都是真实案例,代码都是从真实项目获取后再简化处理的结果 —— 中国人不骗中国人。

场景

下文中以异常打印这个动作作为背景进行代码的探讨

JS版异常处理

由于我用JS都是很久之前的事情了,当时处理异常的时候大致是下面这种思路:

function ex() {
  throw new Error('This is a err!');
}

function testEx() {
  try {
    ex();
  } catch (e) {
    console.log(e); // [object Error]
  }
}

testEx();

首先,第一版没啥好说的,这里为啥打印[object Error]就不多说了。虽然这个知识点比较简单,但当时也可能过于自信或者自己都没有好好验证,代码确实是这么写的。

改进一下:

function ex() {
  throw new Error('This is a err!');
}

function testEx() {
  try {
    ex();
  } catch (e) {
    console.log(e.toString()); // "Error: This is a err!"
  }
}

testEx();

这里调用error的toString()方法和message属性的结果都差不多,反正这么一改,异常的具体信息就出来了。把这些信息放到error日志中,很长的一段时间这段代码都运行的挺好。甚至从JS换成TS后,我跟这段代码一直相安无事。

不知道toString()方法和message属性的可以通过下面代码自行脑补一下:

Error.prototype.toString = function()
{
  "use strict";
  var obj = Object(this);
  if (obj !== this)
    throw new TypeError();
  var name = this.name;
  name = (name === undefined) ? "Error" : String(name);
  var msg = this.message;
  msg = (msg === undefined) ? "" : String(msg);
  if (name === "")
    return msg;
  if (msg === "")
    return name;
  return name + ": " + msg;
};

TS版本

直到前一阵子,我好死不死地对Typescript版本做了一次升级,印象中大概是3.9.x -> 4.4.x,然后,我的代码中报了从未见过的提示异常。

1.png

丢~这个error什么时候变成unknown类型了?不清楚unknown类型的可以在这里补一下课。(补充一下,代码编译完正常运行没啥问题,但是我就是不喜欢这个红波浪的告警,看着贼闹心)

unknown这玩意经常跟any放在一起对比,看似unknown可以被当做任意类型使用,但骨子里其实“啥也不是”——你慢慢品,看是不是这么个道理。unknown在使用之前,必须被转化其它类型,负责一点的解释就是unknown必须通过类型守卫才能进行使用。所以,之前我们手里的是any类型的异常,想怎么处理都行;现在换成unknown了,使用起来处处受制。就好比原来你手里的是RMB,想买啥买啥;现在有人把你手里的RMB强制换成了一种没有人认识的货币。你说他是不是钱,好像也是。但是你用想用它买俩馒头,卖馒头的说不好意思,你得先保证你这玩意能花,并且能够交易馒头才行。从此,你买任何东西都得打给人家打保票。

就咱这倔脾气能忍不?必须不能啊,你说换就换啊,我就不要这个不认识的钱,我要我之前的那个RMB。于是我把捕获的类型换成any

2.png

立马告警就消失了,心想:小样儿吧你!然后biuld一下。

3.png

这回感情好,直接biu不过了。问题大意就是不允许给error定义类型,这么霸道的么?为啥啊?

揭晓谜底的时刻

前面那一大段都是铺垫,接下来才真的进入主题了。我们先看下隐藏在TS背后的“小手”都干了点啥?这一系列迷之操作究竟为了啥?

unknow

unknow应该是TS在3.x出来的一个概念,说实话,这玩意一经问世不温不火,我一度也找不到这东西到底有啥适合的场景。因为没有使用场景,自然用得少,甚至经常忽略它的存在。甚至在TS的版本声明文件中,这家伙的身影也少的可怜。

TS 4.0

但是在TS4.0的版本声明中有一点,有感兴趣的可以自己扒下原文。

4.png

里面大致的意思是:长此以往,catch中的error一直都是any类型滴。当你一旦捕获了异常以后,你爱咋地就咋地,想咋地就咋地。本来是让你捕获异常的,但是经不住你在catch语句里头浪里个浪,又整出一堆活儿。所以,TS首次允许大家使用unknow作为error的类型。这样会要求你在catch块中必须先确认error的类型,再对error做出一些列的操作。不了解为啥要先确定类型再使用的,可以再自行学习一波:为啥要先确定类型。或者看看下面的代码你就明白了,否则catch块中随处都有可能踩雷!

下面的代码说明了如果异常类型是any的话,那么catch子句中可以对x进行任意操作,这样会导致catch子句中又产生新的问题。

try {
    // ...
}
catch (x) {
    // x has type 'any' - have fun!
    console.log(x.message);
    console.log(x.toUpperCase());
    x++;
    x.yadda.yadda.yadda();
}

所以4.0版本以后,允许你在catch子句中通过unknow修饰异常,当然这次只是允许,你沿用之前的用法也没啥问题,只是TS推荐你这么用而已。可以看到,TS的小手第一次尝试动我们手里的“蛋糕”了,只不过这次动的还比较含蓄。

TS 4.4

如果4.0还不算啥,紧接着的TS4.4版本中,直接把catch语句里error的默认类型设置成unknow了。

5.png

这下酸爽了,4.0版本建议你用你不用,是不是觉得给你脸了,这下不用挣扎了。TS一下小手变大手,把你摁在地上可以彻底躺平了。此时,你所有对error的操作都会变成如下这种情况:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

解决方案

先充分表示一波对TS的理解

想来想想TS这样做是不是多此一举?在JS的世界中,你其实可以抛出任何东西。

throw 123;
throw 'What the xxx!?'
throw { '升职加薪': '不批!' }
throw null
throw undefined
throw new Promise(() => {})

所以,代码中我们是无法使用一个确定类型去限制error究竟会是什么。所以长此以往,这个error一直都是any类型。any类型虽然灵活,但是TS4.0中提到的问题确实存在。你甚至都不知道你捕获的异常到底是什么,那谈何去管理它?你可能觉得你自己的代码相当健壮,你绝对不会再在代码中胡乱throw东西的,但是你能保证所有人都这样么?你能保证所有你使用的三方库都这样么?

所以,当处在这种管也不行不管也不行的局面的时候,unknow可能是目前看来规范大家行为一个比较合适的方案了。

通过as any处理

try {
    // ...
}
catch (x) {
    const err = x as any;
    console.log(err.message);
    console.log(err.toUpperCase());
    err++;
    err.yadda.yadda.yadda();
}

这种方案最便捷、最廉价也最没用。相当于变相又将error打回JS模式了。怎么说呢,如果你对自己的代码足够自信,用就用吧,反正出了事我也不背锅。

通过类型守卫处理

类型守卫可以缩小变量的类型,并在守卫作用域内将变量的类型自动锁定为守卫类型。下面的示例还处理error不是Error类型的情况。对于随意抛出的异常做了一定的保护。

try {
    // ...
}
catch (error) {
    let errMsg;
    if (error instanceof Error) {
    	errMsg = error.message;
    } else {
    	// 处理非Error类型异常
    	message = String(error);
    }
    // your error handler logic
}

改良版本

定义异常类型

export type CommonError = {
  message: string;
};

定义异常守卫函数

这里排除了异常为null、undefined等非标准场景,由于error类型是严格按照unknown类型处理的,所以判断的最后使用error as Record<string, unknown>类型转换判断error是否有message属性。

export function isCommonError(error: unknown): error is CommonError {
  return typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as Record<string, unknown>).message === 'string';
}

处理非Error的场景

对于Error处理的函数我放到工具类中,这个方法放在哪里没有特定要求,只是我个人习惯而已。getErrorMessage就干一件事,就是尽可能获取到可以被展示的异常信息。convertToCommonError函数中会先判断当前error是不是上文定义的CommonError类型,如果不是,则想办法将其转化为可被显示的字符串类型。

export default class BaseUtils {

  // ...

  public static getErrorMessage(error: unknown): string {
    return this.convertToCommonError(error).message;
  }

  private static convertToCommonError(error: unknown): CommonError {
    if (isCommonError(error)) {
      return error;
    }
    try {
      return new Error(JSON.stringify(error));
    } catch (error) {
      // 如果抛出的异常不是object
      return new Error(String(error));
    }
  }

  // ...
}

如何使用

这段示例代码相对多一点,但是已经尽可能地处理了大部分error场景,大家可以自行选择使用,算是一件一劳永逸的事情。至少在TS做出新的改变前,它可以帮你尽可能地减少问题的出现。

try {
  // Do ur logic
} catch (error) {
  const errMsg = BaseUtils.getErrorMessage(error);
  // your error handler logic
}

总结

虽然最终的解决方案也不是100%无懈可击,但是在一步步探索为什么的时候你会有那种从单杀,到双杀最后到超神的快感。本文所述的内容掺杂了许多我的个人见解,不一定对;只是希望可以展示一种新的学习知识的思路。当枯燥的知识让你百思不得其解的时候,不妨可以扒扒它们背后的原因,或许会让你豁然开朗。