2025年8月7日

自定义 Error,扩展 Error

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

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

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

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

扩展 Error

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

这里是一个可用的 json 的例子:

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

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

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

我们的 ValidationError 类应该继承自 Error 类。

Error 类是内建的,但我们可以通过下面这段近似代码理解我们要扩展的内容:

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

现在让我们从其中继承 ValidationError 试一试:

class ValidationError extends Error {
  constructor(message) {
    super(message); // (1)
    this.name = "ValidationError"; // (2)
  }
}

function test() {
  throw new ValidationError("Whoops!");
}

try {
  test();
} catch(err) {
  alert(err.message); // Whoops!
  alert(err.name); // ValidationError
  alert(err.stack); // 一个嵌套调用的列表,每个调用都有对应的行号
}

请注意:在 (1) 行中我们调用了父类的 constructor。JavaScript 要求我们在子类的 constructor 中调用 super,所以这是必须的。父类的 constructor 设置了 message 属性。

父类的 constructor 还将 name 属性的值设置为了 "Error",所以在 (2) 行中,我们将其重置为了右边的值。

让我们尝试在 readUser(json) 中使用它吧:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

// 用法
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new ValidationError("No field: age");
  }
  if (!user.name) {
    throw new ValidationError("No field: name");
  }

  return user;
}

// try..catch 的工作示例

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No field: name
  } else if (err instanceof SyntaxError) { // (*)
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // 未知的 error,再次抛出 (**)
  }
}

上面代码中的 try..catch 块既处理我们的 ValidationError 又处理来自 JSON.parse 的内建 SyntaxError

请看一下我们是如何使用 instanceof 来检查 (*) 行中的特定错误类型的。

我们也可以看看 err.name,像这样:

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

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

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

深入继承

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