当我们开发一些东西时,我们经常需要我们自己的错误类来反映任务中可能出错的具体内容。对于网络操作中的错误,我们可能需要 HttpError
,对于数据库操作 DbError
,对于搜索操作 NotFoundError
,依此类推。
我们的错误应该支持基本的错误属性,如 message
、name
,最好还有 stack
。但它们也可能有自己的其他属性,例如 HttpError
对象可能有一个 statusCode
属性,其值如 404
或 403
或 500
。
JavaScript 允许使用 throw
和任何参数,因此从技术上讲,我们的自定义错误类不需要继承自 Error
。但如果我们继承,那么就可以使用 obj instanceof Error
来识别错误对象。所以最好继承自它。
随着应用程序的增长,我们自己的错误自然会形成一个层次结构。例如,HttpTimeoutError
可能继承自 HttpError
,依此类推。
扩展 Error
作为一个示例,让我们考虑一个函数 readUser(json)
,它应该读取包含用户数据的 JSON。
下面是一个有效的 json
的示例
let json = `{ "name": "John", "age": 30 }`;
在内部,我们将使用 JSON.parse
。如果它接收到格式错误的 json
,则会抛出 SyntaxError
。但即使 json
语法正确,也不意味着它是一个有效的用户,对吧?它可能缺少必要数据。例如,它可能没有对我们的用户至关重要的 name
和 age
属性。
我们的函数 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
函数的目的是“读取用户数据”。在此过程中可能会发生不同类型的错误。现在我们有 SyntaxError
和 ValidationError
,但将来 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
函数生成多种类型的错误,那么我们应该自问:我们是否真的想每次逐个检查所有错误类型?
答案通常是“否”:我们希望“高于所有这些”。我们只想了解是否存在“数据读取错误”——具体原因通常无关紧要(错误消息会对其进行描述)。或者,更好的是,我们希望有一种方法来获取错误详细信息,但仅在我们需要时才获取。
我们在此处描述的技术称为“包装异常”。
- 我们将创建一个新类
ReadError
来表示一个通用的“数据读取”错误。 - 函数
readUser
将捕获其内部发生的数据读取错误,例如ValidationError
和SyntaxError
,并生成ReadError
。 ReadError
对象将在其cause
属性中保留对原始错误的引用。
然后,调用 readUser
的代码只需要检查 ReadError
,而不必检查每种数据读取错误。如果它需要更多错误详细信息,它可以检查其 cause
属性。
以下是定义 ReadError
并演示其在 readUser
和 try..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
,但这并不是严格必需的。
评论
<code>
标记,对于多行 – 将它们包装在<pre>
标记中,对于超过 10 行 – 使用沙箱 (plnkr、jsbin、codepen…)