错误处理,"try...catch"
不管你多么精通编程,有时我们的脚本总还是会出现错误。可能是因为我们的编写出错,或是与预期不同的用户输入,或是错误的服务端响应以及其他数千种原因。
通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来。
但是有一种语法结构 try...catch
,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉。
"try...catch" 语法
try...catch
结构由两部分组成:try
和 catch
:
try {
// 代码...
} catch (err) {
// 错误捕获
}
它按照以下步骤执行:
- 首先,执行
try {...}
中的代码。 - 如果这里没有错误,则忽略
catch (err)
:执行到try
的末尾并跳过catch
继续执行。 - 如果这里出现错误,则
try
执行停止,控制流转向catch (err)
的开头。变量err
(我们可以使用任何名称)将包含一个 error 对象,该对象包含了所发生事件的详细信息。
所以,try {...}
块内的 error 不会杀死脚本 —— 我们有机会在catch
中处理它。
try...catch
仅对运行时的 error 有效
要使得try...catch
能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。
如果代码包含语法错误,那么try..catch
将无法正常工作,例如含有不匹配的花括号:
try {
{{{{{{{{{{{{
} catch (err) {
alert("引擎无法理解这段代码,它是无效的");
}
JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。
所以,try...catch
只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。
try...catch
同步执行
如果在“计划的(scheduled)”代码中发生异常,例如在setTimeout
中,则try...catch
不会捕获到异常:
try {
setTimeout(function() {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (err) {
alert( "不工作" );
}
因为
try...catch
包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了try...catch
结构。
为了捕获到计划的(scheduled)函数中的异常,那么try...catch
必须在这个函数内:
setTimeout(function() {
try {
noSuchVariable; // try...catch 处理 error 了!
} catch {
alert( "error 被在这里捕获了!" );
}
}, 1000);
Error对象
发生错误时,JavaScript 会生成一个包含有关此 error 详细信息的对象。然后将该对象作为参数传递给 catch
:
try {
// ...
} catch (err) { // <-- “error 对象”,也可以用其他参数名代替 err
// ...
}
对于所有内建的 error,error 对象具有两个主要属性:
name
Error 名称。例如,对于一个未定义的变量,名称是 "ReferenceError"
。
message
关于 error 的详细文字描述。
还有其他非标准的属性在大多数环境中可用。其中被最广泛使用和支持的是:
stack
当前的调用栈:用于调试目的的一个字符串,其中包含有关导致 error 的嵌套调用序列的信息。
try {
lalala; // error, variable is not defined!
} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// 也可以将一个 error 作为整体显示出来
// error 信息被转换为像 "name: message" 这样的字符串
alert(err); // ReferenceError: lalala is not defined
}
可选的 "catch" 绑定
最近新增的特性
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
如果我们不需要 error 的详细信息,catch
也可以忽略它:
try {
// ...
} catch { // <-- 没有 (err)
// ...
}
使用 "try...catch"
让我们一起探究一下真实场景中 try...catch
的用例。
正如我们所知道的,JavaScript 支持 JSON.parse(str) 方法来解析 JSON 编码的值。
通常,它被用来解析从网络、服务器或是其他来源接收到的数据。
我们收到数据后,然后调用 JSON.parse
如果 json
格式错误,JSON.parse
就会生成一个 error,因此脚本就会“死亡”。
我们对此满意吗?当然不!
如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)。代码执行失败却没有提示信息,这真的是很糟糕的用户体验。
让我们用 try...catch
来处理这个 error:
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- 当出现 error 时...
alert( user.name ); // 不工作
} catch (err) {
// ...执行会跳转到这里并继续执行
alert( "很抱歉,数据有错误,我们会尝试再请求一次。" );
alert( err.name );
alert( err.message );
}
在这儿,我们将 catch
块仅仅用于显示信息,但我们可以做更多的事:发送一个新的网络请求,向访问者建议一个替代方案,将有关错误的信息发送给记录日志的设备,……。所有这些都比代码“死掉”好得多。
抛出自定义 error
如果这个 json
在语法上是正确的,但是没有所必须的 name
属性该怎么办?
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); // <-- 没有 error
alert( user.name ); // 没有 name!
} catch (err) {
alert( "doesn't execute" );
}
这里 JSON.parse
正常执行,但缺少 name
属性对我们来说确实是个 error。
为了统一进行 error 处理,我们将使用 throw
操作符。
"throw" 操作符
throw
操作符会生成一个 error 对象。
throw <error object>
技术上讲,我们可以将任何东西用作 error 对象。甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有 name
和 message
属性的对象(某种程度上保持与内建 error 的兼容性)。
JavaScript 中有很多内建的标准 error 的构造器:Error
,SyntaxError
,ReferenceError
,TypeError
等。我们也可以使用它们来创建 error 对象。
它们的语法是:
let error = new Error(message);
// 或
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
对于内建的 error(不是对于其他任何对象,仅仅是对于 error),name
属性刚好就是构造器的名字。message
则来自于参数(argument)。
let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O
让我们来看看 JSON.parse
会生成什么样的 error:
try {
JSON.parse("{ bad json o_O }");
} catch(err) {
alert(err.name); // SyntaxError
alert(err.message); // Unexpected token b in JSON at position 2
}
正如我们所看到的, 那是一个 SyntaxError
。
在我们的示例中,缺少 name
属性就是一个 error,因为用户必须有一个 name
。
所以,让我们抛出这个 error。
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); // <-- 没有 error
if (!user.name) {
throw new SyntaxError("数据不全:没有 name"); // (*)
}
alert( user.name );
} catch(err) {
alert( "JSON Error: " + err.message ); // JSON Error: 数据不全:没有 name
}
在 (*)
标记的这一行,throw
操作符生成了包含着我们所给定的 message
的 SyntaxError
,与 JavaScript 自己生成的方式相同。try
的执行立即停止,控制流转向 catch
块。
现在,catch
成为了所有 error 处理的唯一场所:对于 JSON.parse
和其他情况都适用。
再次抛出 Rethrowing
在上面的例子中,我们使用 try...catch
来处理不正确的数据。但是在 try {...}
块中是否可能发生 另一个预料之外的 error?例如编程错误(未定义变量)或其他错误,而不仅仅是这种“不正确的数据”。
let json = '{ "age": 30 }'; // 不完整的数据
try {
user = JSON.parse(json); // <-- 忘记在 user 前放置 "let"
// ...
} catch (err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (实际上并没有 JSON Error)
}
当然,一切皆有可能!程序员也会犯错。即使是被数百万人使用了几十年的开源项目中,也可能突然被发现了一个漏洞,并导致可怕的黑客入侵。
在我们的例子中,try...catch
旨在捕获“数据不正确”的 error。但实际上,catch 会捕获到 所有 来自于 try
的 error。在这儿,它捕获到了一个预料之外的 error,但仍然抛出的是同样的 "JSON Error"
信息。这是不正确的,并且也会使代码变得更难以调试。
为了避免此类问题,我们可以采用“重新抛出”技术。规则很简单:
catch
应该只处理它知道的 error,并“抛出”所有其他 error。
“再次抛出(rethrowing)”技术可以被更详细地解释为:
- Catch 捕获所有 error。
- 在
catch (err) {...}
块中,我们对 error 对象err
进行分析。 - 如果我们不知道如何处理它,那我们就
throw err
。
通常,我们可以使用instanceof
操作符判断错误类型:
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {
alert('ReferenceError'); // 访问一个未定义(undefined)的变量产生了 "ReferenceError"
}
}
我们还可以从 err.name
属性中获取错误的类名。所有原生的错误都有这个属性。另一种方式是读取 err.constructor.name
。
在下面的代码中,我们使用“再次抛出”,以达到在 catch
中只处理 SyntaxError
的目的:
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("数据不全:没有 name");
}
blabla(); // 预料之外的 error
alert( user.name );
} catch (err) {
if (err instanceof SyntaxError) {
alert( "JSON Error: " + err.message );
} else {
throw err; // 再次抛出 (*)
}
}
如果 (*)
标记的这行 catch
块中的 error 从 try...catch
中“掉了出来”,那么它也可以被外部的 try...catch
结构(如果存在)捕获到,如果外部不存在这种结构,那么脚本就会被杀死。
所以,catch
块实际上只处理它知道该如何处理的 error,并“跳过”所有其他的 error。
try...catch...finally
等一下,以上并不是所有内容。
try...catch
结构可能还有一个代码子句(clause):finally
。
如果它存在,它在所有情况下都会被执行:
try
之后,如果没有 error,catch
之后,如果有 error。
该扩展语法如下所示:
try {
... 尝试执行的代码 ...
} catch (err) {
... 处理 error ...
} finally {
... 总是会执行的代码 ...
}
finally
子句(clause)通常用在:当我们开始做某事的时候,希望无论出现什么情况都要完成完成某个任务。
例如,我们想要测量一个斐波那契数字函数 fib(n)
执行所需要花费的时间。通常,我们可以在运行它之前开始测量,并在运行完成时结束测量。但是,如果在该函数调用期间出现 error 该怎么办?特别是,下面这段 fib(n)
的实现代码在遇到负数或非整数数字时会返回一个 error。
无论如何,finally
子句都是一个结束测量的好地方。
在这儿,finally
能够保证在两种情况下都能正确地测量时间 —— 成功执行 fib
以及 fib
中出现 error 时:
let num = +prompt("输入一个正整数?", 35)
let diff, result;
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("不能是负数,并且必须是整数。");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
let start = Date.now();
try {
result = fib(num);
} catch (err) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "出现了 error");
alert( `执行花费了 ${diff}ms` );
你可以通过运行上面这段代码并在 prompt
弹窗中输入 35
来进行检查 —— 代码运行正常,先执行 try
然后是 finally
。如果你输入的是 -1
—— 将立即出现 error,执行将只花费 0ms
。以上两种情况下的时间测量都正确地完成了。
换句话说,函数 fib
以 return
还是 throw
完成都无关紧要。在这两种情况下都会执行 finally
子句。
变量和
try...catch...finally
中的局部变量
请注意,上面代码中的result
和diff
变量都是在try...catch
之前 声明的。
否则,如果我们使用let
在try
块中声明变量,那么该变量将只在try
块中可见。
finally
和return
finally
子句适用于try...catch
的 任何 出口。这包括显式的return
。
在下面这个例子中,在try
中有一个return
。在这种情况下,finally
会在控制转向外部代码前被执行。
function func() {
try {
return 1;
} catch (err) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // 先执行 finally 中的 alert,然后执行这个 alert
try...finally
没有catch
子句的try...finally
结构也很有用。当我们不想在这儿处理 error(让它们 fall through),但是需要确保我们启动的处理需要被完成。
function func() {
// 开始执行需要被完成的操作(比如测量)
try {
// ...
} finally {
// 完成前面我们需要完成的那件事,即使 try 中的执行失败了
}
}
上面的代码中,由于没有
catch
,所以try
中的 error 总是会使代码执行跳转至函数func()
外。但是,在跳出之前需要执行finally
中的代码。
全局 catch
环境特定
这个部分的内容并不是 JavaScript 核心的一部分。
设想一下,在 try...catch
结构外有一个致命的 error,然后脚本死亡了。这个 error 就像编程错误或其他可怕的事儿那样。
有什么办法可以用来应对这种情况吗?我们可能想要记录这个 error,并向用户显示某些内容(通常用户看不到错误信息)等。
规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用。例如,Node.JS 有 process.on("uncaughtException")
。在浏览器中,我们可以将一个函数赋值给特殊的 window.onerror 属性,该函数将在发生未捕获的 error 时执行。
语法如下:
window.onerror = function(message, url, line, col, error) {
// ...
};
message
error 信息。
url
发生 error 的脚本的 URL。
line
,col
发生 error 处的代码的行号和列号。
error
error 对象。
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};
function readData() {
badFunc(); // 啊,出问题了!
}
readData();
</script>
全局错误处理程序 window.onerror
的作用通常不是恢复脚本的执行 —— 如果发生编程错误,恢复脚本的执行几乎是不可能的,它的作用是将错误信息发送给开发者。
也有针对这种情况提供 error 日志的 Web 服务,例如 https://errorception.com 或 http://www.muscula.com。
它们会像这样运行:
- 我们注册该服务,并拿到一段 JavaScript 代码(或脚本的 URL),然后插入到页面中。
- 该 JavaScript 脚本设置了自定义的
window.onerror
函数。 - 当发生 error 时,它会发送一个此 error 相关的网络请求到服务提供方。
- 我们可以登录到服务方的 Web 界面来查看这些 error。
自定义 Error,扩展 Error
当我们在开发某些东西时,经常会需要我们自己的 error 类来反映在我们的任务中可能出错的特定任务。对于网络操作中的 error,我们需要 HttpError
,对于数据库操作中的 error,我们需要 DbError
,对于搜索操作中的 error,我们需要 NotFoundError
,等等。
我们自定义的 error 应该支持基本的 error 的属性,例如 message
,name
,并且最好还有 stack
。但是它们也可能会有其他属于它们自己的属性,例如,HttpError
对象可能会有一个 statusCode
属性,属性值可能为 404
、403
或 500
等。
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 在语法上是正确的,也不意味着该数据是有效的用户数据,对吧?因为它可能丢失了某些必要的数据。例如,对用户来说,必不可少的是 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)
行中我们调用了父类的 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
属性的值为一个字符串而不是数字)。让我们针对缺少属性的错误来制作一个更具体的 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 生成的。
请注意,在 PropertyRequiredError
constructor 中的 this.name
是通过手动重新赋值的。这可能会变得有些乏味 —— 在每个自定义 error 类中都要进行 this.name = <class name>
赋值操作。我们可以通过创建自己的“基础错误(basic error)”类来避免这种情况,该类进行了 this.name = this.constructor.name
赋值。然后让所有我们自定义的 error 都从这个“基础错误”类进行继承。
让我们称之为 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
现在自定义的 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 类型?
通常答案是 “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;
}
}
在上面的代码中,readUser
正如所描述的那样正常工作 —— 捕获语法和验证(validation)错误,并抛出 ReadError
(对于未知错误将照常再次抛出)。
所以外部代码检查 instanceof ReadError
,并且它的确是。不必列出所有可能的 error 类型。
这种方法被称为“包装异常(wrapping exceptions)”,因为我们将“低级别”的异常“包装”到了更抽象的 ReadError
中。它被广泛应用于面向对象的编程中。