不管你多么精通编程,有时我们的脚本总还是会出现错误。可能是因为我们的编写出错,或是与预期不同的用户输入,或是错误的服务端响应以及其他数千种原因。
通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来。
但是有一种语法结构 try..catch
,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉。
“try…catch” 语法
try..catch
结构由两部分组成:try
和 catch
:
try {
// 代码...
} catch (err) {
// 错误捕获
}
try {
// 代码...
} catch (err) {
// 错误捕获
}
它按照以下步骤执行:
- 首先,执行
try {...}
中的代码。 - 如果这里没有错误,则忽略
catch(err)
:执行到try
的末尾并跳过catch
继续执行。 - 如果这里出现错误,则
try
执行停止,控制流转向catch(err)
的开头。变量err
(我们可以使用任何名称)将包含一个error
对象,该对象包含了所发生事件的详细信息。
所以,try {…} 块内的错误不会杀死脚本 — 我们有机会在 catch 中处理它。
让我们来看一些例子。
- 没有
error
的例子:显示alert (1)
和(2)
:
try {
alert('Start of try runs'); // (1) // ...这里没有 error
alert('End of try runs'); // (2)
} catch(err) {
alert('Catch is ignored, because there are no errors'); // (3)
}
try {
alert('Start of try runs'); // (1) // ...这里没有 error
alert('End of try runs'); // (2)
} catch(err) {
alert('Catch is ignored, because there are no errors'); // (3)
}
- 包含 error 的例子:显示 (1) 和 (3) 行的 alert 中的内容:
try {
alert('Start of try runs'); // (1)
lalala; // Error,变量未定义!
alert('End of try (never reached)'); // (2)
} catch(err) {
alert(`Error has occurred!`); // (3)
}
try {
alert('Start of try runs'); // (1)
lalala; // Error,变量未定义!
alert('End of try (never reached)'); // (2)
} catch(err) {
alert(`Error has occurred!`); // (3)
}
try..catch 仅对运行时的 error 有效 要使得
try..catch
能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。
如果代码包含语法错误,那么 try..catch
将无法正常工作,例如含有不匹配的花括号:
try {
{{{{{{{{{{{{
} catch(e) {
alert("The engine can't understand this code, it's invalid");
}
try {
{{{{{{{{{{{{
} catch(e) {
alert("The engine can't understand this code, it's invalid");
}
JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。
所以,try..catch
只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。
try..catch 同步工作 如果在“计划的(scheduled)”代码中发生异常,例如在 setTimeout 中,则 try..catch 不会捕获到异常:
try {
setTimeout(function() {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (e) {
alert( "won't work" );
}
try {
setTimeout(function() {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (e) {
alert( "won't work" );
}
因为 try..catch
包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了 try..catch
结构。
为了捕获到计划的(scheduled)函数中的异常,那么 try..catch
必须在这个函数内:
setTimeout(function() {
try {
noSuchVariable; // try..catch 处理 error 了!
} catch {
alert( "error is caught here!" );
}
}, 1000);
setTimeout(function() {
try {
noSuchVariable; // try..catch 处理 error 了!
} catch {
alert( "error is caught here!" );
}
}, 1000);
Error 对象
发生错误时,JavaScript 生成一个包含有关其详细信息的对象。然后将该对象作为参数传递给 catch:
try {
// ...
} catch(err) { // // ...
}
try {
// ...
} catch(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 作为整体显示出来as a whole
// Error 信息被转换为像 "name: message" 这样的字符串
alert(err); // ReferenceError: lalala is not defined
}
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 作为整体显示出来as a whole
// Error 信息被转换为像 "name: message" 这样的字符串
alert(err); // ReferenceError: lalala is not defined
}
可选的 “catch” 绑定
如果我们不需要 error 的详细信息,catch 也可以忽略它:
try {
// ...
} catch { // // ...
}
try {
// ...
} catch { // // ...
}
使用 “try…catch”
让我们一起探究一下真实场景中 try..catch
的用例。
正如我们所知道的,JavaScript 支持 JSON.parse(str)
方法来解析 JSON 编码的值。
通常,它被用来解析从网络,从服务器或是从其他来源接收到的数据。
我们收到数据后,然后像下面这样调用 JSON.parse:
let json = '{"name":"John", "age": 30}'; // 来自服务器的数据
let user = JSON.parse(json); // 将文本表示转换成 JS 对象
// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age ); // 30
let json = '{"name":"John", "age": 30}'; // 来自服务器的数据
let user = JSON.parse(json); // 将文本表示转换成 JS 对象
// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age ); // 30
如果 json 格式错误,JSON.parse 就会生成一个 error,因此脚本就会“死亡”。
我们对此满意吗?当然不!
如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)。代码执行失败却没有提示信息,这真的是很糟糕的用户体验。
让我们用 try..catch 来处理这个 error:
let json = "{ bad json }";
try {
let user = JSON.parse(json); //
alert( user.name ); // 不工作
} catch (e) {// ...执行会跳转到这里并继续执行
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( e.name );
alert( e.message );
}
let json = "{ bad json }";
try {
let user = JSON.parse(json); //
alert( user.name ); // 不工作
} catch (e) {// ...执行会跳转到这里并继续执行
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( e.name );
alert( e.message );
}
在这儿,我们将 catch 块仅仅用于显示信息,但是我们可以做更多的事儿:发送一个新的网络请求,向访问者建议一个替代方案,将有关错误的信息发送给记录日志的设备,……。所有这些都比代码“死掉”好得多。
抛出我们自定义的 error
如果这个 json
在语法上是正确的,但是没有所必须的 name
属性该怎么办?
像这样:
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); //
alert( user.name ); // 没有 name!
} catch (e) {
alert( "doesn't execute" );
}
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); //
alert( user.name ); // 没有 name!
} catch (e) {
alert( "doesn't execute" );
}
这里 JSON.parse
正常执行,但是缺少 name
属性对我们来说确实是个 error
。
为了统一进行 error
处理,我们将使用 throw
操作符。
“Throw” 操作符
throw
操作符会生成一个 error
对象。
语法如下:
throw
技术上讲,我们可以将任何东西用作 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);
// ...
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
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(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token b in JSON at position 2
}
try {
JSON.parse("{ bad json o_O }");
} catch(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token b in JSON at position 2
}
正如我们所看到的, 那是一个 SyntaxError
。
在我们的示例中,缺少 name
属性就是一个 error
,因为用户必须有一个 name
。
所以,让我们抛出这个 error
。
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); // if (!user.name) {throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json); // if (!user.name) {throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}
在 (*)
标记的这一行,throw
操作符生成了包含着我们所给定的 message
的 SyntaxError
,与 JavaScript
自己生成的方式相同。try
的执行立即停止,控制流转向 catch
块。
现在,catch
成为了所有 error
处理的唯一场所:对于 JSON.parse
和其他情况都适用。
try…catch…finally
等一下,以上并不是所有内容。
try..catch
结构可能还有一个代码子句(clause):finally
。
如果它存在,它在所有情况下都会被执行:
- try 之后,如果没有 error,
- catch 之后,如果没有 error。
该扩展语法如下所示:
try {
... 尝试执行的代码 ...
} catch(e) {
... 处理 error ...
} finally {
... 总是会执行的代码 ...
}
try {
... 尝试执行的代码 ...
} catch(e) {
... 处理 error ...
} finally {
... 总是会执行的代码 ...
}
试试运行这段代码:
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
这段代码有两种执行方式:
如果你对于 “Make an error?” 的回答是 “Yes”,那么执行 try -> catch -> finally
。如果你的回答是 “No”,那么执行 try -> finally
。finally
子句(clause)通常用在:当我们开始做某事的时候,希望无论出现什么情况都要完成完成某个任务。
例如,我们想要测量一个斐波那契数字函数 fib(n)
执行所需要花费的时间。通常,我们可以在运行它之前开始测量,并在运行完成时结束测量。但是,如果在该函数调用期间出现 error
该怎么办?特别是,下面这段 fib(n)
的实现代码在遇到负数或非整数数字时会返回一个 error
。
无论如何,finally
子句都是一个结束测量的好地方。
在这儿,finally
能够保证在两种情况下都能正确地测量时间 — 成功执行 fib
以及 fib
中出现 error
时:
let num = +prompt("Enter a positive integer number?", 35)
let diff, result;
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 occurred");
alert( `execution took ${diff}ms` );
let num = +prompt("Enter a positive integer number?", 35)
let diff, result;
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 occurred");
alert( `execution took ${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 (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // 先执行 finally 中的 alert,然后执行这个 alert
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // 先执行 finally 中的 alert,然后执行这个 alert
try..finally
没有catch
子句的try..finally
结构也很有用。当我们不想在这儿处理 error(让它们 fall through),但是需要确保我们启动的处理需要被完成。
function func() {
// 开始执行需要被完成的操作(比如测量)
try {
// ...
} finally {
// 完成前面我们需要完成的那件事儿,即使 try 中的执行失败了
}
}
function func() {
// 开始执行需要被完成的操作(比如测量)
try {
// ...
} finally {
// 完成前面我们需要完成的那件事儿,即使 try 中的执行失败了
}
}
上面的代码中,由于没有 catch
,所以 try
中的 error 总是会使代码执行跳转至函数 func()
外。但是,在跳出之前需要执行 finally
中的代码。
全局 catch
设想一下,在 try..catch
结构外有一个致命的 error,然后脚本死亡了。这个 error 就像编程错误或其他可怕的事儿那样。
有什么办法可以用来应对这种情况吗?我们可能想要记录这个 error,并向用户显示某些内容(通常用户看不到错误信息)等。
规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用。例如,Node.JS 有 process.on("uncaughtException")
。在浏览器中,我们可以将将一个函数赋值给特殊的 window.onerror
属性,该函数将在发生未捕获的 error
时执行。
语法如下:
window.onerror = function(message, url, line, col, error) {
// ...
};
window.onerror = function(message, url, line, col, error) {
// ...
};
message
Error 信息。
url
发生 error 的脚本的 URL。
line,col
发生 error 处的代码的行号和列号。
error
Error 对象。