通常,对于 JavaScript 脚本而言,如果运行脚本时出现错误会挂掉,即引擎会在错误代码处停下来,不会继续执行后续代码,而是将错误信息打印到控制台。比如:
| 1 | let a = 1; | 
上面的代码中,执行到 (*) 这一行便会停下来,将错误信息打印到控制台。
try…catch 结构
但是,有一种语法结构 try...catch 允许我们捕获错误并作出相应的处理,这样脚本在出现错误时不会挂掉,而是执行我们设定的错误处理代码。
我们来看两个例子:
| 1 | try { | 
上面的代码中,因为 try 语句块没有错误,所以 catch 语句块内的代码会被忽略,不会执行。
| 1 | try { | 
上面的代码中,由于 try 语句块内存在错误:变量未定义,所以 try 语句块内这一行之后的代码都不会执行,直接跳转到 catch 语句块内执行错误处理代码。
try…catch 只能捕获运行时错误
所谓运行时错误 runtime-error,是指有效的 JavaScript 代码,即 JavaScript 引擎可以正确解析的代码。对于一个 JavaScript 脚本,引擎首先会解析它,接着执行它。如果出现解析时错误,通常是语法错误,引擎会直接报错,因为引擎这时无法读懂代码,自然地,try..catch 结构不可能捕获到解析错误。比如:
| 1 | try { | 
try…catch 是同步执行的
在诸如定时器 setTimeout 等异步代码中发生错误,try...catch 结构无法捕获错误。比如:
| 1 | try { | 
原因在于,setTimeout 的回调函数在执行时,引擎实际上已经离开了 try...catch 结构体。要捕获类似这样的错误,需要这样做:
| 1 | setTimeout(function() { | 
错误对象
当发生运行时错误时,引擎会创建一个错误对象,里面包含了有关这次错误的信息。该错误对象会被当作参数传递给 catch 语句:
| 1 | try { | 
错误对象主要有 2 个属性:
- name错误的名称,对于未定义的变量而言,是引用错误- ReferenceError。
- message有关错误详情的文本信息。
还有一个非标准但是被广泛采用的属性:
- stack主要用作调试,包含了导致错误的调用栈跟踪。
实例
让我们来看一个实际的例子:解析从服务器获取的 JSON 数据。正常的情况下,应该是这样的:
| 1 | const json = '{"name":"John", "age": 30}'; // data from the server | 
JSON 格式错误
但是实际情况往往复杂多变,首先考虑一种情况,假如 JSON 数据不合法(格式错误,无法被正确解析),那么脚本运行到解析 JSON 数据时将会直接挂掉。这显然不是我们想要的结果,这也会让用户非常困惑。我们可以使用 try...catch 来进行错误处理:
| 1 | const json = "{ bad json }"; | 
抛出错误
再考虑另一种情况:JSON 格式是对的,但是不包含我们需要的字段,在这里是 name 字段:
| 1 | const json = '{ "age": 30 }'; // incomplete data | 
对于这种情况,我们可以使用 throw 操作符来抛出错误:
| 1 | const json = '{ "age": 30 }'; // incomplete data | 
重新抛出错误
接着考虑更复杂的情况,除了 JSON 数据字段缺失的错误,假如 try 语句块内还有其他的错误,比如未定义的变量,如何在 catch 语句块内处理这种情况?接着上面的例子:
| 1 | const json = '{ "age": 30 }'; // incomplete data | 
上面代码标有 (*) 的一行有一个未定义的变量,于是引擎会创建错误对象并跳转到 catch 语句块。需要明确的一点是,catch 会从 try 中捕获所有的错误。对于类似上面的例子,解决思路很简单:catch 语句块应该只处理它知道的错误并重新抛出其他错误。
这一过程大致如下:
- catch会捕获- try内的所有错误。
- 在 catch语句块内,我们通过错误对象的name属性来分析错误。
- 只处理我们知道如何处理的错误,重新抛出其他错误。
针对上面的提到的同时存在未定义变量错误和 JSON 语法错误,我们只需要处理 JSON 语法错误,而将其他错误重新抛出:
| 1 | const json = '{ "age": 30 }'; // incomplete data | 
上面代码中,try...catch 只处理了它关心的 JSON 语法错误,而将其他错误重新抛出。那么其他错误最终到哪里去了呢?
两种可能:如果外部代码没有使用 try...catch 来捕获错误,那么会导致脚本挂掉;如果外部代码使用了 try...catch 结构,则会捕获重新抛出的错误。如下面代码所示:
| 1 | function readData() { | 
上面的代码中,内层的 try...catch 只处理了语法错误,其他的错误都由外层的 try...catch 来处理。
注意事项
- 在 - try...catch...finally语句块内声明的变量只在该语句块没可见。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24- let num = +prompt("Enter a positive integer number?", 35) 
 let diff, result; // 注意这里变量都声明在 try...catch...finally 语句块之外
 function fib(n) {
 if (n < 0 || Math.trunc(n) != n) {
 throw new Error("Must not be negative, and also an integer.");
 }
 return n <= 1 ? n : fib(n - 1) + fib(n - 2);
 }
 let start = Date.now();
 try {
 result = fib(num);
 } catch (e) {
 result = 0;
 } finally {
 diff = Date.now() - start;
 }
 alert(result || "error occured");
 alert( `execution took ${diff}ms` );
- finally语句块总是会执行,即使- try语句块内有显式的返回:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- function func() { 
 try {
 return 1;
 } catch (e) {
 /* ... */
 } finally {
 alert( "finally" );
 }
 }
 alert( func() ); // first works alert from finally, and then this one
全局捕获错误
有一个不属于语言规范,但是各大浏览器都实现了的全局捕获错误的回调函数 window.onerror。它的主要作用不是为了让脚本可以继续执行,而是通常用作错误报告,即将错误信息发送给开发者。在页面中插入下面的脚本,即可实现错误报告的效果:
| 1 | <script> | 
定制和扩展错误
在实际开发中,语言内置的几个标准错误类,比如 Error,SyntaxError,TypeError,ReferenceError 等,可能不足以满足我们在特定情况下的需要。比如在进行网络请求操作时我们可能需要 HttpError,在进行数据库操作时我们可能需要 DbError,对于搜索操作可能需要 NotFoundError 等。我们可以通过继承通用错误类 Error 来定制我们需要的错误类,这被认为是最佳实践。有以下优点:
- 可以继承 message,name,stack这些基础的错误属性。
- 可以使用 inctanceof运算符来判断错误类型。
- 便于之后的多级错误类型继承的形成。
当然,对于不同的错误类,我们可以添加额外所需的属性,比如对于 HttpError,可以添加 statusCode 属性,它的值可能是 404,500 等。
扩展错误实例
让我们来看一个读取 JSON 格式的用户数据的例子。假定我们期望的用户数据是这样的:
| 1 | const json = `{ "name": "John", "age": 30 }`; | 
先做一点铺垫,内置的通用错误类 Error 的伪代码可能是这样的:
| 1 | // The "pseudocode" for the built-in Error class defined by JavaScript itself | 
为了将 JSON 数据字段缺失的错误单独处理,我们定制一个单独的 ValidationError 错误类:
| 1 | class ValidationError extends Error { | 
接着我们将它用在读取用户数据的例子上:
| 1 | class ValidationError extends Error { | 
注意上面代码使用 instanceof 运算符来判断错误类型的做法。
进一步扩展错误类
上面的 ValidationError 错误类还是过于通用,我们在它的基础上继续扩展一个更具体的属性缺失错误类 PropertyRequireError:
| 1 | class ValidationError extends Error { | 
现在,我们在抛出属性错误的时候只需要传入缺失的属性就可以了。还有一个地方可以优化,每次扩展一个类都需要设置 this.name = ...,可以增加一个继承的层级来专门完成这个任务:
| 1 | class MyError extends Error { | 
包装异常
让我们思考一下,readUser 这个函数的任务是从 JSON 数据读取到我们所需要的用户数据字段。让我们站在 readUser 函数的调用者的角度来思考,我们希望得到的错误信息应该简单清晰,是一个类似 ReadError 这样的错误类。至于错误的具体细节应该封装在这个错误类内部,可能是 JSON 格式错误,可能是属性缺失错误,以及将来可能出现的其他错误。
| 1 | class MyError extends Error { | 
上面代码所使用的方式叫做包装异常 Wrapping Exceptions,是一种在面向对象编程中广泛使用的技巧。