原文链接:http://javascript.info/try-catch,translate with ❤️ by zhangbao.

不管我们在编程方面有多伟大,有时我们的脚本都有错误。它们可能会发生,因为我们的错误、意外的用户输入、错误的服务器响应以及其他许多原因。

通常,在出现错误时,脚本“死亡”(立即停止),将其打印到控制台。

但是有一个语法结构 try..catch 允许去捕获(catch)错误,而不是死亡,从而做一些更合理的事情。

“try..catch”语法

try..catch 构造有两个主要块:trycatch

  1. try {
  2. // code...
  3. } catch (err) {
  4. // error handling
  5. }

其过程如下:

  1. 首先,执行 try {...} 中的代码。

  2. 如果没有错误发生的话,catch(err) 部分就会被忽略:代码执行到 try 块结尾,然后跳过 catch

  3. 如果发生错误,try 中的代码执行就会停止,控制权流入了 catch(err)。变量 err(或任何你起的名字)包含一个错误对象和详情被传递进来。

错误处理,“try..catch” - 图1

因此,try {...} 块中的错误并没有杀死脚本,我们有机会在 catch 中去做相应处理。

我们来看更多的例子:

  • 一个正确的例子:展示 alert (1)(2) 处:
  1. try {
  2. alert('Start of try runs'); // (1) <--
  3. // ...没有错误发生
  4. alert('End of try runs'); // (2) <--
  5. } catch(err) {
  6. alert('Catch is ignored, because there are no errors'); // (3)
  7. }
  8. alert("...Then the execution continues");
  • 一个有错误的例子:展示在 (1)(3)
  1. try {
  2. alert('Start of try runs'); // (1) <--
  3. lalala; // 出错,变量未定义
  4. alert('End of try (never reached)'); // (2)
  5. } catch(err) {
  6. alert(`Error has occured!`); // (3) <--
  7. }
  8. alert("...Then the execution continues");

注意:try..catch 只适用于运行时错误

try..catch 要想工作,代码必须是可运行的。换句话说,它应该是有效的 JavaScript。

如果代码在语法上是错误的,那么它就不会起作用,例如它有不匹配的花括号:

  1. try {
  2. {{{{{{{{{{{{
  3. } catch(e) {
  4. alert("The engine can't understand this code, it's invalid");
  5. }

JavaScript 引擎首先读取代码,然后运行它。在阅读短语中出现的错误被称为“parse-time”错误,并且是不可恢复的(从代码中)。这是因为引擎无法理解代码。

所以 try..catch 只能处理有效代码中发生的错误,这些错误被称为“运行时错误”,有时也称为“异常”。

注意:try..catch 代码是同步执行的

如果在“预定”代码中出现异常,像 setTimeout 中的异常,try..catch 并不能捕获:

  1. try {
  2. setTimeout(function () {
  3. noSuchVariable; // 脚本在这个地方停止执行
  4. }, 1000);
  5. } catch (err) {
  6. alert('不会执行这里的代码');
  7. }

这是因为 try..catch 实际上包装的回调函数的 setTimeout。回调函数是在之后执行的,这个时候,引擎已经离开了 try..catch 结构。

为了捕获预定函数里的异常,try..catch 必须在回调函数里使用:

  1. setTimeout(function() {
  2. try {
  3. noSuchVariable; // try..catch handles the error!
  4. } catch (e) {
  5. alert( "error is caught here!" );
  6. }
  7. }, 1000);

Error 对象

当发生错误时,JavaScript 会生成一个包含其详细信息的对象。然后,该对象作为一个参数传递给 catch

  1. try {
  2. // ...
  3. } catch(err) { // <-- the "error object", could use another word instead of err
  4. // ...
  5. }

对于所有的内置错误对象,主要有两个属性:

name

异常名。对未定义变量异常来说,是“ReferenceError”。

message``

关于错误细节的文本消息。

在大多数环境中还有其他非标准属性。最广泛使用和支持的是:

stack

当前调用堆栈:带有关于导致错误的嵌套调用序列的信息的字符串,用于调试使用。

例如:

  1. try {
  2. lalala; // error, variable is not defined!
  3. } catch(err) {
  4. alert(err.name); // ReferenceError
  5. alert(err.message); // lalala is not defined
  6. alert(err.stack); // ReferenceError: lalala is not defined at ...
  7. // Can also show an error as a whole
  8. // The error is converted to string as "name: message"
  9. alert(err); // ReferenceError: lalala is not defined
  10. }

使用“try..catch”

我们来看下实际生活里的 try..catch 的使用方式。

我们已经知道,JavaScript 支持 JSON.parse(str) 方法用来读取 JSON 形式的字符串数据。

它通常用来解码从网络、服务器端或者其他资源传递过来的数据。

下面我们来接收数据,并且调用 JSON.parse,像这样:

  1. let json = '{"name":"John", "age": 30}'; // 从服务器端接收到的数据
  2. let user = JSON.parse(json); // 将 JSON 文本转换为 JS 对象
  3. // 现在 user 是一个拥有属性的对象了
  4. alert( user.name ); // John
  5. alert( user.age ); // 30

你可以在 JSON 方法,toJSON 章节里发现更多关于 JSON 的更多细节数据。

如果 json 是无效格式的,JSON.parse 就会产生错误,因此就此“停止”。

难道我们就这样容忍错误的发生吗?当然不是!

这种方式,如果数据有哪点不对的地方,访问者不会知道(除非打开开发者控制台)。大家肯定不想在脚本“停止”的时候就那么停止了,而没有接收到任何错误细信息。

下面我们来用 try..catch 来处理错误:

  1. let json = "{ bad json }";
  2. try {
  3. let user = JSON.parse(json); // <-- when an error occurs...
  4. alert( user.name ); // doesn't work
  5. } catch (e) {
  6. // ...the execution jumps here
  7. alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  8. alert( e.name );
  9. alert( e.message );
  10. }

我们用 catch 块显示信息,但是我们可以做更多的事情:发送一个新的网络请求,向访问者提出一个替代方案,将关于错误的信息发送到日志工具……反正比垂死要好。

抛出我们自己的错误

但是如果 json 语法没问题,但是不包含必要的 name 属性怎么办呢?

想这样:

  1. let json = '{ "age": 30 }'; // 不完全数据
  2. try {
  3. let user = JSON.parse(json); // <-- 没有错误
  4. alert( user.name ); // 没有 name!
  5. } catch (e) {
  6. alert( "doesn't execute" );
  7. }

这里的 JSON.parse 正常执行了,但是缺少 name 属性对我们来说同样是错误。

为了统一错误处理,我们将使用 throw 操作符。

“throw”操作符

throw 用来生成错误。

语法如下:

  1. throw <error object>

从技术上将,我们可以使用任何类型值作为错误抛出。可能是一个原始值,像数字或者字符串,但是最好是对象,最好还有 namemessage 属性(为了与内置错误对象达到兼容)。

JavaScript 内置了许多标准错误对象类型:ErrorSyntaxErrorReferenceErrorTypeError 等等。我们也可以使用它们来创建错误对象。

语法是这样的:

  1. let error = new Error(message);
  2. // 或者
  3. let error = new SyntaxError(message);
  4. let error = new ReferenceError(message);
  5. // ...

对于内置的错误(不是针所有对象,只是针对错误对象),name 属性正是对应构造函数名,message 属性值就是我们传入的参数值。

例如:

  1. let error = new Error('Things happen o_O');
  2. alert(error.name); // Error
  3. alert(error.message); // Things happen o_O

让我们看看 JSON.parse 生成的错误类型:

  1. try {
  2. JSON.parse("{ bad json o_O }");
  3. } catch(e) {
  4. alert(e.name); // SyntaxError
  5. alert(e.message); // Unexpected token o in JSON at position 0
  6. }

我们能看到,是 SyntaxError

在我们的例子中,如果用户必须要有一个 name 属性,name 的缺失也可以被看作是一个语法错误。

所以我们抛出错误:

  1. let json = '{ "age": 30 }'; // incomplete data
  2. try {
  3. let user = JSON.parse(json); // <-- no errors
  4. if (!user.name) {
  5. throw new SyntaxError("Incomplete data: no name"); // (*)
  6. }
  7. alert( user.name );
  8. } catch(e) {
  9. alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
  10. }

(*) 行中,throw 操作符会用给定的 message 的产生 SyntaxError,就像 JavaScript 自己生成的一样。try 中代码执行会立即停止,控制流进入 catch

现在,catch 变成了所有错误处理的单一位置:JSON.parse 和其他情况下。

重新抛出

在上面的例子里,我们使用 try..catch 处理不正确数据。但是在 try {...} 块中有没有可能发生另一个意料不到的错误呢?像是一个变量没有定义或者怎样,而不仅仅是“数据不正确”。

类似:

  1. let json = '{ "age": 30 }'; // incomplete data
  2. try {
  3. let user = JSON.parse(json);
  4. blabla(); // 在这里发生错误了·
  5. // ...
  6. } catch(err) {
  7. alert("JSON Error: " + err); // JSON Error: ReferenceError: blabla is not defined
  8. // (no JSON Error actually)
  9. }

当然,一切都是可能的!程序员会犯错误。即使是在数百万人使用的开源工具中,也可能会突然出现一个疯狂的 bug,导致可怕的黑客攻击(就像 ssh 工具所发生的那样)。

在之前情况里,我们用 try..catch 捕获“数据不正确”错误。但是从本质上来说,catch 捕获从 try 中发生的 所有错误。但结果是,我们得到了一个意料之外的错误,但却提示同样的“JSON Error”错误消息,这就不对了,也让 debug 变得困难。

幸运的是,通过错误对象的 name 属性,我们能够知道发生的是什么错误:

  1. try {
  2. user;
  3. } catch(e) {
  4. alert(e.name); // "ReferenceError"
  5. }

规则很简单:

catch 只用来处理它理解的错误类型,其他错误类型直接“重新抛出”。

重新抛出”技术可以解释为以下内容:

  1. 捕获所有错误。

  2. catch(err) {...} 块中分析错误对象 err

  3. 如果我们不知道怎么处理它,就 throw err

在下面代码里,我们在 catch 中仅处理 SyntaxError 错误,其他类型错误就抛出:

  1. let json = '{ "age": 30 }'; // incomplete data
  2. try {
  3. let user = JSON.parse(json);
  4. if (!user.name) {
  5. throw new SyntaxError("Incomplete data: no name");
  6. }
  7. blabla(); // 意料之外的错误
  8. alert( user.name );
  9. } catch(e) {
  10. if (e.name == "SyntaxError") {
  11. alert( "JSON Error: " + e.message );
  12. } else {
  13. throw e; // rethrow (*)
  14. }
  15. }

(*) 处的重新抛出的错误,会被外部的 try..catch 结构捕获到(有的话),或者脚本直接停止。

所以 catch 块实际上只处理它知道如何处理的错误,而“跳过”所有其他类型错误。

下面的例子演示了如何通过一个更高层次的 try..catch 来捕获这个错误。

  1. function readData() {
  2. let json = '{ "age": 30 }';
  3. try {
  4. // ...
  5. blabla(); // error!
  6. } catch (err) {
  7. // ...
  8. if (err.name != 'SyntaxError') {
  9. throw err; // rethrow (don't know how to deal with it)
  10. }
  11. }
  12. }
  13. try {
  14. readData();
  15. } catch (err) {
  16. alert( "External catch got: " + err ); // caught it!
  17. }

在这里,readData 只知道如何处理 SyntaxError,而外部 try..catch 知道如何处理所有事情。

try..catch…finally

等等,还没说完。

try..catch 构造还可以支持再一个子句:finally

如果存在的话,会按照如下情景执行:

  • 如果没有错误,在 try 之后执行,

  • 如果有错误,在 catch 之后执行。

扩展语法是这样的:

  1. try {
  2. // ... try to execute the code ...
  3. } catch (err) {
  4. // ... handle errors ...
  5. } finally {
  6. // ... execute always ...
  7. }

试着运行这段代码:

  1. try {
  2. alert( 'try' );
  3. if (confirm('Make an error?')) BAD_CODE();
  4. } catch (e) {
  5. alert( 'catch' );
  6. } finally {
  7. alert( 'finally' );
  8. }

代码有两种执行方式:

  1. 如果你回答 YES 主动来“犯错误”,是按照 try -> catch -> finally 的顺序。

  2. 如果你说 No,执行顺序是 try -> finally 的顺序。

finally 子句通常用这样的场景:我们在 try..catch 之前开始做了一些事情,在每次从 try..catch 出来之后都要进行结束处理的场景。

例如:例如,我们想要测量 Fibonacci 数函数 fib(n) 执行所花费的时间。很自然地,我们可以在它跑完之前开始测量,但是如果在函数调用中出现了错误呢?特别地,在下面的代码中,fib(n) 支持返回一个负数或非整数的错误。

finally 是一个无论如何取完成测量的好地方。

这里 finally 保证了在两种情况下都能正确地测量时间——在成功执行 fib 的情况,或出现错误的情况:

  1. let num = +prompt("Enter a positive integer number?", 35);
  2. let diff, result;
  3. function fib(n) {
  4. if (n < 0 || Math.trunc(n) != n) {
  5. throw new Error("Must not be negative, and also an integer.");
  6. }
  7. return n <= 1 ? n : fib(n - 1) + fib(n - 2);
  8. }
  9. let start = Date.now();
  10. try {
  11. result = fib(num);
  12. } catch (e) {
  13. result = 0;
  14. } finally {
  15. diff = Date.now() - start;
  16. }
  17. alert(result || "error occured");
  18. alert( `execution took ${diff}ms` );

您可以运行代码,用默认输入的 35 来检查——代码会正常执行,finallytry 后执行。如果输入 -1,会立即产生一个错误,执行耗费 0ms。这两个情况下的测量都是正确的。

换句话说,可能有两种退出函数的方法:return 或者 throwfinally 子句会处理这两种情况。

注:try..catch..finally 中的变量是本地的

请注意,上述代码中的 resultdiff 变量是在 try..catch 之前声明的。

如果这两个变量是在 {...} 块中用 let 声明的,那么这两个变量只在内部可见。

注:finallyreturn

finally 子句适用于任何 try..catch 情况的退出,这还包括显式的 return

在下面的例子里,try 里有个 return。在这种情况下,finally 在控制权返回到外部代码之前执行。

  1. function func() {
  2. try {
  3. return 1;
  4. } catch (e) {
  5. /* ... */
  6. } finally {
  7. alert( 'finally' );
  8. }
  9. }
  10. alert( func() ); // 先 alert 出 finally,最后是 1

注:try..finally

try..catch 构造,如果没有 catch 子句,也是有使用场景的。当我们不想处理错误时,我们使用它,但是要确保我们开始的过程已经完成了。

  1. function func() {
  2. // 开始做些需要完成的任务(像测量)
  3. try {
  4. // ...
  5. } finally {
  6. // 即使脚本失败了,也要完成任务
  7. }
  8. }

在上面的代码中,try 中的失败总是导致外溢(因为没有捕获)。但 finally 总会在控制流跳出外部之前执行。

全局捕获

本节讲述的“全局捕获”是特定环境特性,不是 JavaScript 核心里的一部分。

让我们想象一下,我们在 try..catch 之外有一个致命的错误,导致脚本停止执行了,比如编程错误或其他糟糕的事情。

是否有办法对此类事件作出反应?我们可能想要记录错误,向用户显示一些东西(通常他不会看到错误消息)等等。

在规范中没有说明处理,但在有些环境通常会提供它,因为它非常有用。例如 Node.js 有提供 process.on(‘unaughtexception’) 方法。在浏览器中,我们可以为 window.onerror 属性分配一个特殊函数,它会在出现未捕获错误的情况下运行。

语法:

  1. window.onerror = function(message, url, line, col, error) {
  2. // ...
  3. };

message

错误消息。

url

错误发生的脚本的URL。

linecol

出现错误的行和列号。

error

错误对象。

例如:

  1. <script>
  2. window.onerror = function(message, url, line, col, error) {
  3. alert(`${message}\n At ${line}:${col} of ${url}`);
  4. };
  5. function readData() {
  6. badFunc(); // Whoops, something went wrong!
  7. }
  8. readData();
  9. </script>

全局处理程序 window.onerror 通常不会恢复脚本的执行——在编程错误的情况下,这可能是不可能的,但是可以将错误消息发送给开发人员。

还有一些 web 服务为此类情况提供了错误日志记录,比如 https://errorception.comhttp://www.muscula.com。

他们的工作原理是这样的:

  1. 我们在服务中注册并从它们中获取一个 JS(或一个脚本 URL),以便插入到页面上。

  2. JS脚本有一个自定义 window.onerror 函数。

  3. 当发生错误时,它会向服务发送一个关于它的网络请求。

  4. 我们可以登录到服务 web 界面并查看错误。

总结

try..``catch 构造允许处理运行时错误。从字面上理解,就是尝试运行代码并捕获可能出现的错误。

语法如下:

  1. try {
  2. // run this code
  3. } catch(err) {
  4. // if an error happened, then jump here
  5. // err is the error object
  6. } finally {
  7. // do in any case after try/catch
  8. }

可以没有 catch 部分,也没有 finally,所以 try..catchtry..finally 都是有效的。

错误对象具有以下属性:

  • message:人类可读的错误消息。

  • name:表示错误名称的字符串(错误构造函数名)。

  • stack(非标准):错误堆栈。

我们也可以使用 throw 操作符来生成我们自己的错误。从技术上讲, throw 的参数可以是任何东西,但通常它是继承自内置错误类的错误对象。更多扩展错误对象的知识在下一章中会讲到。

重新抛出是错误处理的基本模式:一个 catch 块通常期望并知道如何处理特定的错误类型,所以它也会抛出它不知道的错误对象。

即使我们没有使用 try..catch,大多数环境允许设置一个“全局”错误处理程序来捕获“退出”的错误。在浏览器中,它是 window.onerror

(完)