2023 年 8 月 7 日

自定义错误,扩展 Error

当我们开发一些东西时,我们经常需要我们自己的错误类来反映任务中可能出错的具体内容。对于网络操作中的错误,我们可能需要 HttpError,对于数据库操作 DbError,对于搜索操作 NotFoundError,依此类推。

我们的错误应该支持基本的错误属性,如 messagename,最好还有 stack。但它们也可能有自己的其他属性,例如 HttpError 对象可能有一个 statusCode 属性,其值如 404403500

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

随着应用程序的增长,我们自己的错误自然会形成一个层次结构。例如,HttpTimeoutError 可能继承自 HttpError,依此类推。

扩展 Error

作为一个示例,让我们考虑一个函数 readUser(json),它应该读取包含用户数据的 JSON。

下面是一个有效的 json 的示例

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

在内部,我们将使用 JSON.parse。如果它接收到格式错误的 json,则会抛出 SyntaxError。但即使 json 语法正确,也不意味着它是一个有效的用户,对吧?它可能缺少必要数据。例如,它可能没有对我们的用户至关重要的 nameage 属性。

我们的函数 readUser(json) 不仅会读取 JSON,还会检查(“验证”)数据。如果缺少必需字段或格式错误,则会出错。这不是 SyntaxError,因为数据在语法上是正确的,而是另一种错误。我们将称之为 ValidationError 并为其创建一个类。此类错误还应携带有关违规字段的信息。

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

Error 类是内置的,但这里有它的近似代码,以便我们了解我们正在扩展的内容

// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (different names for different built-in error classes)
    this.stack = <call stack>; // non-standard, but most environments support it
  }
}

现在让我们从它继承 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); // a list of nested calls with line numbers for each
}

请注意:在行 (1) 中,我们调用父构造函数。JavaScript 要求我们在子构造函数中调用 super,因此这是必须的。父构造函数设置 message 属性。

父构造函数还将 name 属性设置为 "Error",因此在行 (2) 中,我们将它重置为正确的值。

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

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

// Usage
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;
}

// Working example with 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; // unknown error, rethrow it (**)
  }
}

上面代码中的 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 块只知道如何处理验证和语法错误,其他类型(由代码中的错别字或其他未知原因引起)应贯穿始终。

进一步继承

ValidationError 类非常通用。很多事情都可能出错。属性可能不存在,或者可能格式错误(例如 age 的字符串值而不是数字)。让我们创建一个更具体的类 PropertyRequiredError,专门针对不存在的属性。它将携带有关缺失属性的附加信息。

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

class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("No property: " + property);
    this.name = "PropertyRequiredError";
    this.property = property;
  }
}

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

  if (!user.age) {
    throw new PropertyRequiredError("age");
  }
  if (!user.name) {
    throw new PropertyRequiredError("name");
  }

  return user;
}

// Working example with try..catch

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No property: name
    alert(err.name); // PropertyRequiredError
    alert(err.property); // name
  } else if (err instanceof SyntaxError) {
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // unknown error, rethrow it
  }
}

新的类 PropertyRequiredError 易于使用:我们只需要传递属性名称:new PropertyRequiredError(property)。可读的 message 由构造函数生成。

请注意,PropertyRequiredError 构造函数中的 this.name 再次被手动分配。这可能会变得有点乏味——在每个自定义错误类中分配 this.name = <class name>。我们可以通过创建我们自己的“基本错误”类来避免这种情况,该类分配 this.name = this.constructor.name。然后从它继承我们所有的自定义错误。

我们称之为 MyError

以下是使用 MyError 和其他自定义错误类的代码,已简化

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

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("No property: " + property);
    this.property = property;
  }
}

// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

现在自定义错误要短得多,尤其是 ValidationError,因为我们在构造函数中去掉了 "this.name = ..." 行。

包装异常

上面代码中 readUser 函数的目的是“读取用户数据”。在此过程中可能会发生不同类型的错误。现在我们有 SyntaxErrorValidationError,但将来 readUser 函数可能会增长,并且可能会生成其他类型的错误。

调用 readUser 的代码应处理这些错误。现在它在 catch 块中使用多个 if,这些 if 检查类并处理已知错误,并重新抛出未知错误。

方案如下

try {
  ...
  readUser()  // the potential error source
  ...
} catch (err) {
  if (err instanceof ValidationError) {
    // handle validation errors
  } else if (err instanceof SyntaxError) {
    // handle syntax errors
  } else {
    throw err; // unknown error, rethrow it
  }
}

在上面的代码中,我们可以看到两种类型的错误,但可能还有更多。

如果 readUser 函数生成多种类型的错误,那么我们应该自问:我们是否真的想每次逐个检查所有错误类型?

答案通常是“否”:我们希望“高于所有这些”。我们只想了解是否存在“数据读取错误”——具体原因通常无关紧要(错误消息会对其进行描述)。或者,更好的是,我们希望有一种方法来获取错误详细信息,但仅在我们需要时才获取。

我们在此处描述的技术称为“包装异常”。

  1. 我们将创建一个新类 ReadError 来表示一个通用的“数据读取”错误。
  2. 函数 readUser 将捕获其内部发生的数据读取错误,例如 ValidationErrorSyntaxError,并生成 ReadError
  3. ReadError 对象将在其 cause 属性中保留对原始错误的引用。

然后,调用 readUser 的代码只需要检查 ReadError,而不必检查每种数据读取错误。如果它需要更多错误详细信息,它可以检查其 cause 属性。

以下是定义 ReadError 并演示其在 readUsertry..catch 中的用法的代码

class ReadError extends Error {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
    this.name = 'ReadError';
  }
}

class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }

function validateUser(user) {
  if (!user.age) {
    throw new PropertyRequiredError("age");
  }

  if (!user.name) {
    throw new PropertyRequiredError("name");
  }
}

function readUser(json) {
  let user;

  try {
    user = JSON.parse(json);
  } catch (err) {
    if (err instanceof SyntaxError) {
      throw new ReadError("Syntax Error", err);
    } else {
      throw err;
    }
  }

  try {
    validateUser(user);
  } catch (err) {
    if (err instanceof ValidationError) {
      throw new ReadError("Validation Error", err);
    } else {
      throw err;
    }
  }

}

try {
  readUser('{bad json}');
} catch (e) {
  if (e instanceof ReadError) {
    alert(e);
    // Original error: SyntaxError: Unexpected token b in JSON at position 1
    alert("Original error: " + e.cause);
  } else {
    throw e;
  }
}

在上面的代码中,readUser 的工作方式与描述完全一致——捕获语法和验证错误,并改而抛出 ReadError 错误(未知错误照常重新抛出)。

因此,外部代码检查 instanceof ReadError,仅此而已。无需列出所有可能的错误类型。

这种方法称为“包装异常”,因为我们采用“低级”异常并将它们“包装”到更抽象的 ReadError 中。它在面向对象编程中广泛使用。

摘要

  • 我们通常可以继承自 Error 和其他内置错误类。我们只需要注意 name 属性,不要忘记调用 super
  • 我们可以使用 instanceof 检查特定错误。它也适用于继承。但有时我们有一个来自第三方库的错误对象,并且没有简单的方法来获取其类。然后,name 属性可用于此类检查。
  • 包装异常是一种广泛使用的方法:函数处理低级异常并创建更高级别的错误,而不是各种低级错误。低级异常有时会成为该对象的属性,如上述示例中的 err.cause,但这并不是严格必需的。

任务

重要性:5

创建一个继承自内置 SyntaxError 类的 FormatError 类。

它应支持 messagenamestack 属性。

用法示例

let err = new FormatError("formatting error");

alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack

alert( err instanceof FormatError ); // true
alert( err instanceof SyntaxError ); // true (because inherits from SyntaxError)
class FormatError extends SyntaxError {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
  }
}

let err = new FormatError("formatting error");

alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack

alert( err instanceof SyntaxError ); // true
教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 – 将它们包装在 <pre> 标记中,对于超过 10 行 – 使用沙箱 (plnkrjsbincodepen…)