原文链接:http://javascript.info/try-catch,translate with ❤️ by zhangbao.
不管我们在编程方面有多伟大,有时我们的脚本都有错误。它们可能会发生,因为我们的错误、意外的用户输入、错误的服务器响应以及其他许多原因。
通常,在出现错误时,脚本“死亡”(立即停止),将其打印到控制台。
但是有一个语法结构 try..catch 允许去捕获(catch)错误,而不是死亡,从而做一些更合理的事情。
“try..catch”语法
try..catch 构造有两个主要块:try 和 catch:
try {// code...} catch (err) {// error handling}
其过程如下:
首先,执行
try {...}中的代码。如果没有错误发生的话,
catch(err)部分就会被忽略:代码执行到try块结尾,然后跳过catch。如果发生错误,
try中的代码执行就会停止,控制权流入了catch(err)。变量err(或任何你起的名字)包含一个错误对象和详情被传递进来。

因此,try {...} 块中的错误并没有杀死脚本,我们有机会在 catch 中去做相应处理。
我们来看更多的例子:
- 一个正确的例子:展示 alert
(1)和(2)处:
try {alert('Start of try runs'); // (1) <--// ...没有错误发生alert('End of try runs'); // (2) <--} catch(err) {alert('Catch is ignored, because there are no errors'); // (3)}alert("...Then the execution continues");
- 一个有错误的例子:展示在
(1)和(3)处
try {alert('Start of try runs'); // (1) <--lalala; // 出错,变量未定义alert('End of try (never reached)'); // (2)} catch(err) {alert(`Error has occured!`); // (3) <--}alert("...Then the execution continues");
注意:try..catch 只适用于运行时错误
try..catch 要想工作,代码必须是可运行的。换句话说,它应该是有效的 JavaScript。
如果代码在语法上是错误的,那么它就不会起作用,例如它有不匹配的花括号:
try {{{{{{{{{{{{{} catch(e) {alert("The engine can't understand this code, it's invalid");}
JavaScript 引擎首先读取代码,然后运行它。在阅读短语中出现的错误被称为“parse-time”错误,并且是不可恢复的(从代码中)。这是因为引擎无法理解代码。
所以 try..catch 只能处理有效代码中发生的错误,这些错误被称为“运行时错误”,有时也称为“异常”。
注意:try..catch 代码是同步执行的
如果在“预定”代码中出现异常,像 setTimeout 中的异常,try..catch 并不能捕获:
try {setTimeout(function () {noSuchVariable; // 脚本在这个地方停止执行}, 1000);} catch (err) {alert('不会执行这里的代码');}
这是因为 try..catch 实际上包装的回调函数的 setTimeout。回调函数是在之后执行的,这个时候,引擎已经离开了 try..catch 结构。
为了捕获预定函数里的异常,try..catch 必须在回调函数里使用:
setTimeout(function() {try {noSuchVariable; // try..catch handles the error!} catch (e) {alert( "error is caught here!" );}}, 1000);
Error 对象
当发生错误时,JavaScript 会生成一个包含其详细信息的对象。然后,该对象作为一个参数传递给 catch:
try {// ...} catch(err) { // <-- the "error object", could use another word instead of err// ...}
对于所有的内置错误对象,主要有两个属性:
name
异常名。对未定义变量异常来说,是“ReferenceError”。
message``
关于错误细节的文本消息。
在大多数环境中还有其他非标准属性。最广泛使用和支持的是:
stack
当前调用堆栈:带有关于导致错误的嵌套调用序列的信息的字符串,用于调试使用。
例如:
try {lalala; // error, variable is not defined!} catch(err) {alert(err.name); // ReferenceErroralert(err.message); // lalala is not definedalert(err.stack); // ReferenceError: lalala is not defined at ...// Can also show an error as a whole// The error is converted to string as "name: message"alert(err); // ReferenceError: lalala is not defined}
使用“try..catch”
我们来看下实际生活里的 try..catch 的使用方式。
我们已经知道,JavaScript 支持 JSON.parse(str) 方法用来读取 JSON 形式的字符串数据。
它通常用来解码从网络、服务器端或者其他资源传递过来的数据。
下面我们来接收数据,并且调用 JSON.parse,像这样:
let json = '{"name":"John", "age": 30}'; // 从服务器端接收到的数据let user = JSON.parse(json); // 将 JSON 文本转换为 JS 对象// 现在 user 是一个拥有属性的对象了alert( user.name ); // Johnalert( user.age ); // 30
你可以在 JSON 方法,toJSON 章节里发现更多关于 JSON 的更多细节数据。
如果 json 是无效格式的,JSON.parse 就会产生错误,因此就此“停止”。
难道我们就这样容忍错误的发生吗?当然不是!
这种方式,如果数据有哪点不对的地方,访问者不会知道(除非打开开发者控制台)。大家肯定不想在脚本“停止”的时候就那么停止了,而没有接收到任何错误细信息。
下面我们来用 try..catch 来处理错误:
let json = "{ bad json }";try {let user = JSON.parse(json); // <-- when an error occurs...alert( user.name ); // doesn't work} catch (e) {// ...the execution jumps herealert( "Our apologies, the data has errors, we'll try to request it one more time." );alert( e.name );alert( e.message );}
我们用 catch 块显示信息,但是我们可以做更多的事情:发送一个新的网络请求,向访问者提出一个替代方案,将关于错误的信息发送到日志工具……反正比垂死要好。
抛出我们自己的错误
但是如果 json 语法没问题,但是不包含必要的 name 属性怎么办呢?
想这样:
let json = '{ "age": 30 }'; // 不完全数据try {let user = JSON.parse(json); // <-- 没有错误alert( user.name ); // 没有 name!} catch (e) {alert( "doesn't execute" );}
这里的 JSON.parse 正常执行了,但是缺少 name 属性对我们来说同样是错误。
为了统一错误处理,我们将使用 throw 操作符。
“throw”操作符
throw 用来生成错误。
语法如下:
throw <error object>
从技术上将,我们可以使用任何类型值作为错误抛出。可能是一个原始值,像数字或者字符串,但是最好是对象,最好还有 name 和 message 属性(为了与内置错误对象达到兼容)。
JavaScript 内置了许多标准错误对象类型:Error、SyntaxError、ReferenceError、TypeError 等等。我们也可以使用它们来创建错误对象。
语法是这样的:
let error = new Error(message);// 或者let error = new SyntaxError(message);let error = new ReferenceError(message);// ...
对于内置的错误(不是针所有对象,只是针对错误对象),name 属性正是对应构造函数名,message 属性值就是我们传入的参数值。
例如:
let error = new Error('Things happen o_O');alert(error.name); // Erroralert(error.message); // Things happen o_O
让我们看看 JSON.parse 生成的错误类型:
try {JSON.parse("{ bad json o_O }");} catch(e) {alert(e.name); // SyntaxErroralert(e.message); // Unexpected token o in JSON at position 0}
我们能看到,是 SyntaxError。
在我们的例子中,如果用户必须要有一个 name 属性,name 的缺失也可以被看作是一个语法错误。
所以我们抛出错误:
let json = '{ "age": 30 }'; // incomplete datatry {let user = JSON.parse(json); // <-- no errorsif (!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 变成了所有错误处理的单一位置:JSON.parse 和其他情况下。
重新抛出
在上面的例子里,我们使用 try..catch 处理不正确数据。但是在 try {...} 块中有没有可能发生另一个意料不到的错误呢?像是一个变量没有定义或者怎样,而不仅仅是“数据不正确”。
类似:
let json = '{ "age": 30 }'; // incomplete datatry {let user = JSON.parse(json);blabla(); // 在这里发生错误了·// ...} catch(err) {alert("JSON Error: " + err); // JSON Error: ReferenceError: blabla is not defined// (no JSON Error actually)}
当然,一切都是可能的!程序员会犯错误。即使是在数百万人使用的开源工具中,也可能会突然出现一个疯狂的 bug,导致可怕的黑客攻击(就像 ssh 工具所发生的那样)。
在之前情况里,我们用 try..catch 捕获“数据不正确”错误。但是从本质上来说,catch 捕获从 try 中发生的 所有错误。但结果是,我们得到了一个意料之外的错误,但却提示同样的“JSON Error”错误消息,这就不对了,也让 debug 变得困难。
幸运的是,通过错误对象的 name 属性,我们能够知道发生的是什么错误:
try {user;} catch(e) {alert(e.name); // "ReferenceError"}
规则很简单:
catch 只用来处理它理解的错误类型,其他错误类型直接“重新抛出”。
“重新抛出”技术可以解释为以下内容:
捕获所有错误。
在
catch(err) {...}块中分析错误对象err。如果我们不知道怎么处理它,就
throw err。
在下面代码里,我们在 catch 中仅处理 SyntaxError 错误,其他类型错误就抛出:
let json = '{ "age": 30 }'; // incomplete datatry {let user = JSON.parse(json);if (!user.name) {throw new SyntaxError("Incomplete data: no name");}blabla(); // 意料之外的错误alert( user.name );} catch(e) {if (e.name == "SyntaxError") {alert( "JSON Error: " + e.message );} else {throw e; // rethrow (*)}}
在 (*) 处的重新抛出的错误,会被外部的 try..catch 结构捕获到(有的话),或者脚本直接停止。
所以 catch 块实际上只处理它知道如何处理的错误,而“跳过”所有其他类型错误。
下面的例子演示了如何通过一个更高层次的 try..catch 来捕获这个错误。
function readData() {let json = '{ "age": 30 }';try {// ...blabla(); // error!} catch (err) {// ...if (err.name != 'SyntaxError') {throw err; // rethrow (don't know how to deal with it)}}}try {readData();} catch (err) {alert( "External catch got: " + err ); // caught it!}
在这里,readData 只知道如何处理 SyntaxError,而外部 try..catch 知道如何处理所有事情。
try..catch…finally
等等,还没说完。
try..catch 构造还可以支持再一个子句:finally。
如果存在的话,会按照如下情景执行:
如果没有错误,在
try之后执行,如果有错误,在
catch之后执行。
扩展语法是这样的:
try {// ... try to execute the code ...} catch (err) {// ... handle errors ...} finally {// ... execute always ...}
试着运行这段代码:
try {alert( 'try' );if (confirm('Make an error?')) BAD_CODE();} catch (e) {alert( 'catch' );} finally {alert( 'finally' );}
代码有两种执行方式:
如果你回答 YES 主动来“犯错误”,是按照
try -> catch -> finally的顺序。如果你说 No,执行顺序是
try -> finally的顺序。
finally 子句通常用这样的场景:我们在 try..catch 之前开始做了一些事情,在每次从 try..catch 出来之后都要进行结束处理的场景。
例如:例如,我们想要测量 Fibonacci 数函数 fib(n) 执行所花费的时间。很自然地,我们可以在它跑完之前开始测量,但是如果在函数调用中出现了错误呢?特别地,在下面的代码中,fib(n) 支持返回一个负数或非整数的错误。
finally 是一个无论如何取完成测量的好地方。
这里 finally 保证了在两种情况下都能正确地测量时间——在成功执行 fib 的情况,或出现错误的情况:
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 occured");alert( `execution took ${diff}ms` );
您可以运行代码,用默认输入的 35 来检查——代码会正常执行,finally 在 try 后执行。如果输入 -1,会立即产生一个错误,执行耗费 0ms。这两个情况下的测量都是正确的。
换句话说,可能有两种退出函数的方法:return 或者 throw,finally 子句会处理这两种情况。
注:try..catch..finally 中的变量是本地的
请注意,上述代码中的 result 和 diff 变量是在 try..catch 之前声明的。
如果这两个变量是在 {...} 块中用 let 声明的,那么这两个变量只在内部可见。
注:finally 和 return
finally 子句适用于任何 try..catch 情况的退出,这还包括显式的 return。
在下面的例子里,try 里有个 return。在这种情况下,finally 在控制权返回到外部代码之前执行。
function func() {try {return 1;} catch (e) {/* ... */} finally {alert( 'finally' );}}alert( func() ); // 先 alert 出 finally,最后是 1
注:try..finally
try..catch 构造,如果没有 catch 子句,也是有使用场景的。当我们不想处理错误时,我们使用它,但是要确保我们开始的过程已经完成了。
function func() {// 开始做些需要完成的任务(像测量)try {// ...} finally {// 即使脚本失败了,也要完成任务}}
在上面的代码中,try 中的失败总是导致外溢(因为没有捕获)。但 finally 总会在控制流跳出外部之前执行。
全局捕获
本节讲述的“全局捕获”是特定环境特性,不是 JavaScript 核心里的一部分。
让我们想象一下,我们在 try..catch 之外有一个致命的错误,导致脚本停止执行了,比如编程错误或其他糟糕的事情。
是否有办法对此类事件作出反应?我们可能想要记录错误,向用户显示一些东西(通常他不会看到错误消息)等等。
在规范中没有说明处理,但在有些环境通常会提供它,因为它非常有用。例如 Node.js 有提供 process.on(‘unaughtexception’) 方法。在浏览器中,我们可以为 window.onerror 属性分配一个特殊函数,它会在出现未捕获错误的情况下运行。
语法:
window.onerror = function(message, url, line, col, error) {// ...};
message
错误消息。
url
错误发生的脚本的URL。
line,col
出现错误的行和列号。
error
错误对象。
例如:
<script>window.onerror = function(message, url, line, col, error) {alert(`${message}\n At ${line}:${col} of ${url}`);};function readData() {badFunc(); // Whoops, something went wrong!}readData();</script>
全局处理程序 window.onerror 通常不会恢复脚本的执行——在编程错误的情况下,这可能是不可能的,但是可以将错误消息发送给开发人员。
还有一些 web 服务为此类情况提供了错误日志记录,比如 https://errorception.com 或http://www.muscula.com。
他们的工作原理是这样的:
我们在服务中注册并从它们中获取一个 JS(或一个脚本 URL),以便插入到页面上。
JS脚本有一个自定义
window.onerror函数。当发生错误时,它会向服务发送一个关于它的网络请求。
我们可以登录到服务 web 界面并查看错误。
总结
try..``catch 构造允许处理运行时错误。从字面上理解,就是尝试运行代码并捕获可能出现的错误。
语法如下:
try {// run this code} catch(err) {// if an error happened, then jump here// err is the error object} finally {// do in any case after try/catch}
可以没有 catch 部分,也没有 finally,所以 try..catch 和 try..finally 都是有效的。
错误对象具有以下属性:
message:人类可读的错误消息。name:表示错误名称的字符串(错误构造函数名)。stack(非标准):错误堆栈。
我们也可以使用 throw 操作符来生成我们自己的错误。从技术上讲, throw 的参数可以是任何东西,但通常它是继承自内置错误类的错误对象。更多扩展错误对象的知识在下一章中会讲到。
重新抛出是错误处理的基本模式:一个 catch 块通常期望并知道如何处理特定的错误类型,所以它也会抛出它不知道的错误对象。
即使我们没有使用 try..catch,大多数环境允许设置一个“全局”错误处理程序来捕获“退出”的错误。在浏览器中,它是 window.onerror。
(完)
