原文链接: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); // ReferenceError
alert(err.message); // lalala is not defined
alert(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 ); // John
alert( 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 here
alert( "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); // Error
alert(error.message); // Things happen o_O
让我们看看 JSON.parse
生成的错误类型:
try {
JSON.parse("{ bad json o_O }");
} catch(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token o in JSON at position 0
}
我们能看到,是 SyntaxError
。
在我们的例子中,如果用户必须要有一个 name
属性,name
的缺失也可以被看作是一个语法错误。
所以我们抛出错误:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
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
变成了所有错误处理的单一位置:JSON.parse
和其他情况下。
重新抛出
在上面的例子里,我们使用 try..catch
处理不正确数据。但是在 try {...}
块中有没有可能发生另一个意料不到的错误呢?像是一个变量没有定义或者怎样,而不仅仅是“数据不正确”。
类似:
let json = '{ "age": 30 }'; // incomplete data
try {
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 data
try {
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
。
(完)