当我们在开发某些东西时,经常会需要我们自己的 error 类来反映在我们的任务中可能出错的特定任务。对于网络操作中的 error,我们需要 HttpError,对于数据库操作中的 error,我们需要 DbError,对于搜索操作中的 error,我们需要 NotFoundError,等等。

我们自定义的 error 应该支持基本的 error 的属性,例如 message,name,并且最好还有 stack。但是它们也可能会有其他属于它们自己的属性,例如,HttpError 对象可能会有一个 statusCode 属性,属性值可能为 404、403 或 500 等。

JavaScript 允许将 throw 与任何参数一起使用,所以从技术上讲,我们自定义的 error 不需要从 Error 中继承。但是,如果我们继承,那么就可以使用 obj instanceof Error 来识别 error 对象。因此,最好继承它。

随着开发的应用程序的增长,我们自己的 error 自然会形成形成一个层次结构(hierarchy)。例如,HttpTimeoutError 可能继承自 HttpError,等等。

扩展 Error

例如,让我们考虑一个函数 readUser(json),该函数应该读取带有用户数据的 JSON。
这里是一个可用的 json 的例子:

  1. let json = `{ "name": "John", "age": 30 }`;

在函数内部,我们将使用 JSON.parse。如果它接收到格式不正确的 json,就会抛出 SyntaxError。但是,即使 json 在语法上是正确的,也不意味着该数据是有效的用户数据,对吧?因为它可能丢失了某些必要的数据。例如,对用户来说,必不可少的是 name 和 age 属性。

我们的函数 readUser(json) 不仅会读取 JSON,还会检查(“验证”)数据。如果没有所必须的字段,或者(字段的)格式错误,那么就会出现一个 error。并且这些并不是 SyntaxError,因为这些数据在语法上是正确的,这些是另一种错误。我们称之为 ValidationError,并为之创建一个类。这种类型的错误也应该包含有关违规字段的信息。

我们的 ValidationError 类应该继承自内建的 Error 类。
Error 类是内建的,但这是其近似代码,所以我们可以了解我们要扩展的内容:

  1. // JavaScript 自身定义的内建的 Error 类的“伪代码”
  2. class Error {
  3. constructor(message) {
  4. this.message = message;
  5. this.name = "Error"; // (不同的内建 error 类有不同的名字)
  6. this.stack = <call stack>; // 非标准的,但大多数环境都支持它
  7. }
  8. }

现在让我们从其中继承 ValidationError,并尝试进行运行:

  1. class ValidationError extends Error {
  2. constructor(message) {
  3. super(message); // (1)
  4. this.name = "ValidationError"; // (2)
  5. }
  6. }
  7. function test() {
  8. throw new ValidationError("Whoops!");
  9. }
  10. try {
  11. test();
  12. } catch(err) {
  13. alert(err.message); // Whoops!
  14. alert(err.name); // ValidationError
  15. alert(err.stack); // 一个嵌套调用的列表,每个调用都有对应的行号
  16. }

请注意:在 (1) 行中我们调用了父类的 constructor。JavaScript 要求我们在子类的 constructor 中调用 super,所以这是必须的。父类的 constructor 设置了 message 属性。
父类的 constructor 还将 name 属性的值设置为了 “Error”,所以在 (2) 行中,我们将其重置为了右边的值。
让我们尝试在 readUser(json) 中使用它吧:

  1. class ValidationError extends Error {
  2. constructor(message) {
  3. super(message);
  4. this.name = "ValidationError";
  5. }
  6. }
  7. // 用法
  8. function readUser(json) {
  9. let user = JSON.parse(json);
  10. if (!user.age) {
  11. throw new ValidationError("No field: age");
  12. }
  13. if (!user.name) {
  14. throw new ValidationError("No field: name");
  15. }
  16. return user;
  17. }
  18. // try..catch 的工作示例
  19. try {
  20. let user = readUser('{ "age": 25 }');
  21. } catch (err) {
  22. if (err instanceof ValidationError) {
  23. alert("Invalid data: " + err.message); // Invalid data: No field: name
  24. } else if (err instanceof SyntaxError) { // (*)
  25. alert("JSON Syntax Error: " + err.message);
  26. } else {
  27. throw err; // 未知的 error,再次抛出 (**)
  28. }
  29. }

上面代码中的 try..catch 块既处理我们的 ValidationError 又处理来自 JSON.parse 的内建 SyntaxError。
请看一下我们是如何使用 instanceof 来检查 (*) 行中的特定错误类型的。
我们也可以看看 err.name,像这样:

  1. // ...
  2. // instead of (err instanceof SyntaxError)
  3. } else if (err.name == "SyntaxError") { // (*)
  4. // ...

使用 instanceof 的版本要好得多,因为将来我们会对 ValidationError 进行扩展,创建它的子类型,例如 PropertyRequiredError。而 instanceof 检查对于新的继承类也适用。所以这是面向未来的做法。

还有一点很重要,在 catch 遇到了未知的错误,它会在 (**) 行将该错误再次抛出。catch 块只知道如何处理 validation 错误和语法错误,而其他错误(由于代码中的错字或其他未知的错误)应该被扔出(fall through)。

深入继承

ValidationError 类是非常通用的。很多东西都可能出错。对象的属性可能缺失或者属性可能有格式错误(例如 age 属性的值为一个字符串)。让我们针对缺少属性的错误来制作一个更具体的 PropertyRequiredError 类。它将携带有关缺少的属性的相关信息。

  1. class ValidationError extends Error {
  2. constructor(message) {
  3. super(message);
  4. this.name = "ValidationError";
  5. }
  6. }
  7. class PropertyRequiredError extends ValidationError {
  8. constructor(property) {
  9. super("No property: " + property);
  10. this.name = "PropertyRequiredError";
  11. this.property = property;
  12. }
  13. }
  14. // 用法
  15. function readUser(json) {
  16. let user = JSON.parse(json);
  17. if (!user.age) {
  18. throw new PropertyRequiredError("age");
  19. }
  20. if (!user.name) {
  21. throw new PropertyRequiredError("name");
  22. }
  23. return user;
  24. }
  25. // try..catch 的工作示例
  26. try {
  27. let user = readUser('{ "age": 25 }');
  28. } catch (err) {
  29. if (err instanceof ValidationError) {
  30. alert("Invalid data: " + err.message); // Invalid data: No property: name
  31. alert(err.name); // PropertyRequiredError
  32. alert(err.property); // name
  33. } else if (err instanceof SyntaxError) {
  34. alert("JSON Syntax Error: " + err.message);
  35. } else {
  36. throw err; // 为止 error,将其再次抛出
  37. }
  38. }

这个新的类 PropertyRequiredError 使用起来很简单:我们只需要传递属性名:new PropertyRequiredError(property)。人类可读的 message 是由 constructor 生成的。

请注意,在 PropertyRequiredError constructor 中的 this.name 是通过手动重新赋值的。这可能会变得有些乏味 — 在每个自定义 error 类中都要进行 this.name = 赋值操作。我们可以通过创建自己的“基础错误(basic error)”类来避免这种情况,该类进行了 this.name = this.constructor.name 赋值。然后让所有我们自定义的 error 都从这个“基础错误”类进行继承。

让我们称之为 MyError。
这是带有 MyError 以及其他自定义的 error 类的代码,已进行简化:

  1. class MyError extends Error {
  2. constructor(message) {
  3. super(message);
  4. this.name = this.constructor.name;
  5. }
  6. }
  7. class ValidationError extends MyError { }
  8. class PropertyRequiredError extends ValidationError {
  9. constructor(property) {
  10. super("No property: " + property);
  11. this.property = property;
  12. }
  13. }
  14. // name 是对的
  15. alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

现在自定义的 error 短了很多,特别是 ValidationError,因为我们摆脱了 constructor 中的 “this.name = …” 这一行。