原文链接:http://javascript.info/custom-errors,translate with ❤️ by zhangbao.
在开发的时候,我们经常需要定义我们自己的错误类型,方便反映发生在项目里的具体错误类型。对于网络操作错误我们可能需要 HttpError
,数据操作错误用 DbError
,查找错误使用 NotFoundError
等等。
我们的自定义应该支持基本的属性,像 message
,name
,嗯,最好还有 stack
。但是也可以有他们自己的一个属性,比如 HttpError
对象可以有 statusCode
属性,其属性值可能为 404
、403
或者 500
。
JavaScript 允许 throw
后面跟任何类型值,所以从技术上讲,自定义错误对象不必非要继承自 Error
。如果我们继承了的话,就可以使用 obj instanceof Error
去识别错误对象。说一说,最好继承自 Error
。
当构建项目时,我们的自定义错误通常也有继承关系。比如,HttpTimeoutError
继承 HttpError
等等。
扩展 Error
作为一个例子,我们考虑函数 readUser(json)
用来读取 JSON 格式的用户数据。
这里是一个有效 JSON 数据格式的例子:
let json = `{ "name": "John", "age": 30 }`;
在内部,我们使用 JSON.parse
解析 JSON 格式文本,如果是无效 JSON 格式,就会抛出 SyntaxError
错误。
但即使 json
没有语法错误,也并不意味着这是一个有效的用户,对吗?它可能会缺少必要字段。例如,可能没有 name
或者 age
属性。
我们的函数 readUser(json)
不仅仅读取 JSON,而且会检查(“验证”)数据。如果缺少了必要的字段,或是格式错误的话,都会产生错误。就是说,错误不是只有 SyntaxError
类型的,因为数据可能是语法有效的,产生的是其他类型的错误。我们叫它 ValidationError
并且用类创建它,这种错误包含有关违规字段信息。
我们的 Validator
类应该继承内置的 Error
类。
这个类是内置的,但是我们应该看看它的伪代码,以了解我们正在扩展的内容。
class Error {
constructor (message) {
this.message = message;
this.name = 'Error'; // 不同的内置错误对象有不同的 name 属性值
this.stack = <nested calls>; // 非标准属性,但是许多环境支持它
}
}
现在我们继续,用 Validator
继承它:
class ValidatorError extends Error {
constructor (message) {
super(message); // (1)
this.name = 'ValidatorError'; // (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";
}
}
// 使用
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; // unknown error, rethrow it (**)
}
}
try..catch
代码块在这里处理了两种错误类型:一个是 Validator
错误;一个是由内置方法 JSON.parse
触发的 SyntaxError
错误。
请注意,我们在 (*)
处使用 instanceof
操作符检查错误类型。
我们也可以检查 err.name
,像这样:
// ...
// 不使用 err instanceof SyntaxError
} else if (err.name === 'SyntaxError') { // (*)
// ...
使用 instanceof
版本代码会更好一点,因为在未来我们可能会扩展 ValidatorError
,从中扩展出子类,像 PropertyRequiredError
。而 instanceof
检查对新子类错误类型检查仍是 OK 的。它不会过时的。
同样重要的是,如果 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;
}
}
// 使用
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; // unknown error, rethrow it
}
}
新类型 PropertyRequiredError
很好使用:我们只需要通过 new PropertyRequiredError(property)
方式传递属性名。人类可读的 message
信息会由构造函数生成。
请注意,PropertyRequiredError
构造器里再一次为 this.name
手工赋值了。这可能会变得有点麻烦——在创建每个自定义错误时都要分配 this.name=<class name>
。但有一条出路,我们可以创建自己的“基本错误”类,通过在构造函数中使用 this.constructor.name
来消除我们肩上的负担,然后后来的子类都继承它。
我们可以假设现叫这个基础类叫 MyError
。
下面是 MyError
和其他自定义错误类的代码,简化了:
class MyError extends Error {
constructor (message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidatorError extends MyError {
constructor (property) {
super('No property: ' + property);
this.property = property;
}
}
// name 属性值是正确的
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
现在的自定义错误要短得多,特别是 ValidationError
,因为我们在构造函数中去掉了 this.name = ...
这一行内容。
包装异常
上面的代码中 readUser
函数的目的是“读取用户数据”,对吗?在这个过程中可能会出现不同类型的错误。现在我们有了 SyntaxError
和 ValidationError
,但是在将来,readUser
函数可能会增长:新的代码可能会产生其他类型的错误。
调用 readUser
的代码应该处理这些错误。现在,它在 catch
块中使用多个 if
来检查不同的错误类型,并重新抛出未知的错误类型。但是如果 readUser
函数产生了几种类型的错误——那么我们应该问自己:我们真的想要在每一个调用 readUser
的代码中逐一检查所有的错误类型吗?
通常答案是“不”:外部代码想要“高于一切”。它想要有某种“数据读取错误”。为什么会发生这种情况——通常是不相关的(错误消息描述了它)。或者,如果有一种方法可以得到错误细节,那就更好了,但前提是我们需要。
因此,让我们创建一个新的类 ReadError
来表示这些错误。如果在 readUser
中发生了错误,我们将在那里捕获并产生 ReadError
。我们还将在其 cause
属性中保留对原始错误的引用。然后,外部代码只需要检查 ReadError
。
下面是定义 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
错误(未知的错误像往常一样被重新抛出)。
所以外部代码检查了 ReadError
的实例,就是这样。不需要列出可能的所有错误类型。
这种方法被称为“包装异常”,因为我们将“低级别异常”“包装”到 ReadError
中,这对于调用代码来说更加抽象和方便,它在面向对象编程中得到了广泛的应用。
总结
我们可以继承
Error
和其他内置错误类型,只需要注意name
属性,也不要忘记调用super
。大多数情况下,我们应该使用
instanceof
来检查特定的错误,它也会检查原型链。但是有时候我们有一个来自第三方库的错误对象,没有简单的方法来得到这个类。然后,此类情况适合使用name
属性检查。包装异常是一种广泛的技术,用函数来处理低层异常,并用一个更高层对象来报告错误。低层异常成为高层对象得一个属性(比如上面例子里得
err.cause
),但这并不是严格要求的。
(完)