一、开发中,经常需要我们自己的 error 类来反映在我们的任务中可能出错的特定任务。
1、对于网络操作中的 error,我们需要HttpError
2、对于数据库操作中的 error,我们需要DbError
3、对于搜索操作中的 error,我们需要NotFoundError…
二、我们自定义的 error 应该支持基本的 error 的属性,例如message,name,并且最好还有stack。但是它们也可能会有其他属于它们自己的属性
1、例如,HttpError对象可能会有一个statusCode属性,属性值可能为404、403或500等。
三、JavaScript 允许将throw与任何参数一起使用
1、所以从技术上讲,我们自定义的 error 不需要从Error中继承。
2、但是,如果我们继承,那么就可以使用obj instanceof Error来识别 error 对象。因此,最好继承它。
四、随着虽开发的应用程序的增长,我们自己的 error 自然会形成形成一个层次结构(hierarchy)。例如,HttpTimeoutError可能继承自HttpError,等等。
自定义Error
一、我们可以正常地从Error和其他内建的 error 类中进行继承,。我们只需要注意name属性以及不要忘了调用super。
继承
一、我们可以使用instanceof来检查特定的 error。但有时我们有来自第三方库的 error 对象,并且在这儿没有简单的方法来获取它的类。那么可以将name属性用于这一类的检查。
包装异常
一、包装异常是一项广泛应用的技术:用于处理低级别异常并创建高级别 error 而不是各种低级别 error 的函数。在上面的示例中,低级别异常有时会成为该对象的属性,例如err.cause,但这不是严格要求的。
自定义Error示例
扩展 Error
一、例如,让我们考虑一个函数readUser(json),该函数应该读取带有用户数据的 JSON。
二、这里是一个可用的json的例子:
let json = `{ "name": "John", "age": 30 }`;
三、在函数内部,我们将使用JSON.parse。如果它接收到格式不正确的json,就会抛出SyntaxError。但是,即使json在语法上是正确的,也不意味着该数据是有效的用户数据,对吧?因为它可能丢失了某些必要的数据。例如,对用户来说,必不可少的是name和age属性。
四、我们的函数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、请注意:在(1)行中我们调用了父类的 constructor。JavaScript 要求我们在子类的 constructor 中调用super,所以这是必须的。父类的 constructor 设置了message属性。
2、父类的 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,再次抛出 (**)
}
}
1、上面代码中的try..catch块既处理我们的ValidationError又处理来自JSON.parse的内建SyntaxError。
2、请看一下我们是如何使用instanceof来检查(*)行中的特定错误类型的。
九、我们也可以看看err.name,像这样:
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
1、使用instanceof的版本要好得多,因为将来我们会对ValidationError进行扩展,创建它的子类型,例如PropertyRequiredError。而instanceof检查对于新的继承类也适用。所以这是面向未来的做法。
2、还有一点很重要,在catch遇到了未知的错误,它会在(**)行将该错误再次抛出。catch块只知道如何处理 validation 错误和语法错误,而其他错误(由于代码中的错字或其他未知的错误)应该被扔出(fall through)。
深入继承
一、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;
}
}
// 用法
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("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 property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // 为止 error,将其再次抛出
}
}
二、这个新的类PropertyRequiredError使用起来很简单:我们只需要传递属性名:new PropertyRequiredError(property)。人类可读的message是由 constructor 生成的。
三、请注意,在PropertyRequiredErrorconstructor 中的this.name是通过手动重新赋值的。这可能会变得有些乏味 — 在每个自定义 error 类中都要进行this.name =
四、让我们称之为MyError。
五、这是带有MyError以及其他自定义的 error 类的代码,已进行简化:
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 是对的
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
1、现在自定义的 error 短了很多,特别是ValidationError,因为我们摆脱了 constructor 中的”this.name = …”这一行。
包装异常
一、在上面代码中的函数readUser的目的就是“读取用户数据”。在这个过程中可能会出现不同类型的 error。目前我们有了SyntaxError和ValidationError,但是将来,函数readUser可能会不断壮大,并可能会产生其他类型的 error。
二、调用readUser的代码应该处理这些 error。现在它在catch块中使用了多个if语句来检查 error 类,处理已知的 error,并再次抛出未知的 error。
三、该方案是这样的:
try {
...
readUser() // 潜在的 error 源
...
} catch (err) {
if (err instanceof ValidationError) {
// 处理 validation error
} else if (err instanceof SyntaxError) {
// 处理 syntax error
} else {
throw err; // 未知 error,再次抛出它
}
}
四、在上面的代码中,我们可以看到两种类型的 error,但是可以有更多。
五、如果readUser函数会产生多种 error,那么我们应该问问自己:我们是否真的想每次都一一检查所有的 error 类型?
1、通常答案是 “No”:我们希望能够“比它高一个级别”。我们只想知道这里是否是“数据读取异常” — 为什么发生了这样的 error 通常是无关紧要的(error 信息描述了它)。或者,如果我们有一种方法能够获取 error 的详细信息那就更好了,但前提是我们需要。
六、我们所描述的这项技术被称为“包装异常”。
- 我们将创建一个新的类ReadError来表示一般的“数据读取” error。
- 函数readUser将捕获内部发生的数据读取 error,例如ValidationError和SyntaxError,并生成一个ReadError来进行替代。
- 对象ReadError会把对原始 error 的引用保存在其cause属性中。
七、之后,调用readUser的代码只需要检查ReadError,而不必检查每种数据读取 error。并且,如果需要更多 error 细节,那么可以检查readUser的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;
}
}
1、在上面的代码中,readUser正如所描述的那样正常工作 — 捕获语法和验证(validation)错误,并抛出ReadError(对于未知错误将照常再次抛出)。
2、所以外部代码检查instanceof ReadError,并且它的确是。不必列出所有可能的 error 类型。
3、这种方法被称为“包装异常(wrapping exceptions)”,因为我们将“低级别”的异常“包装”到了更抽象的ReadError中。它被广泛应用于面向对象的编程中。