无论我们的编程能力有多强,有时我们的脚本都会有错误。它们可能是由于我们的错误、意外的用户输入、错误的服务器响应以及其他一千个原因造成的。
通常,脚本在发生错误时会“死亡”(立即停止),并将其打印到控制台。
但有一个语法结构 try...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) <-- // ...no errors here alert('End of try runs'); // (2) <-- } catch (err) { alert('Catch is ignored, because there are no errors'); // (3) }
-
带有错误的示例:显示
(1)
和(3)
try { alert('Start of try runs'); // (1) <-- lalala; // error, variable is not defined! alert('End of try (never reached)'); // (2) } catch (err) { alert(`Error has occurred!`); // (3) <-- }
try...catch
仅适用于运行时错误要使 try...catch
正常工作,代码必须可运行。换句话说,它应该是有效的 JavaScript。
如果代码语法错误,例如它有不成对的花括号,则它将不起作用
try {
{{{{{{{{{{{{
} catch (err) {
alert("The engine can't understand this code, it's invalid");
}
JavaScript 引擎首先读取代码,然后运行它。在读取阶段发生的错误称为“解析时”错误,并且是不可恢复的(从该代码内部)。这是因为引擎无法理解代码。
因此,try...catch
只能处理有效代码中发生的错误。此类错误称为“运行时错误”或有时称为“异常”。
try...catch
同步工作如果异常发生在“已计划”的代码中,例如在 setTimeout
中,则 try...catch
不会捕获它
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (err) {
alert( "won't work" );
}
这是因为该函数本身稍后执行,此时引擎已经离开了 try...catch
结构。
要在计划函数中捕获异常,try...catch
必须在该函数内部
setTimeout(function() {
try {
noSuchVariable; // try...catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);
错误对象
当发生错误时,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 (...call stack)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
可选的“catch”绑定
如果我们不需要错误详细信息,catch
可以省略它
try {
// ...
} catch { // <-- without (err)
// ...
}
使用“try…catch”
让我们探索一下 try...catch
的实际用例。
正如我们所知,JavaScript 支持 JSON.parse(str) 方法来读取 JSON 编码的值。
通常,它用于解码从服务器或其他来源通过网络接收的数据。
我们接收它并像这样调用 JSON.parse
let json = '{"name":"John", "age": 30}'; // data from the server
let user = JSON.parse(json); // convert the text representation to JS object
// now user is an object with properties from the string
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 (err) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( err.name );
alert( err.message );
}
这里我们只使用 catch
块来显示消息,但我们可以做更多事情:发送新的网络请求、向访问者建议替代方案、将有关错误的信息发送到日志记录工具,……所有这些都比直接死掉要好得多。
抛出我们自己的错误
如果 json
语法正确,但没有必需的 name
属性,该怎么办?
像这样
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (err) {
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);
// or
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 (err) {
alert(err.name); // SyntaxError
alert(err.message); // Unexpected token b in JSON at position 2
}
正如我们所看到的,这是一个 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 (err) {
alert( "JSON Error: " + err.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 {
user = JSON.parse(json); // <-- forgot to put "let" before user
// ...
} catch (err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (no JSON Error actually)
}
当然,一切皆有可能!程序员确实会犯错。即使在数百万用户使用了几十年的开源实用程序中——突然可能会发现一个导致可怕的攻击的错误。
在我们的案例中,try...catch
被放置为捕获“不正确的数据”错误。但从本质上讲,catch
会从 try
中获取所有错误。在这里,它会得到一个意外的错误,但仍然显示相同的 "JSON Error"
消息。这是错误的,并且也使代码更难调试。
为了避免此类问题,我们可以采用“重新抛出”技术。规则很简单
Catch 应该只处理它已知的错误,并“重新抛出”所有其他错误。
“重新抛出”技术可以更详细地解释为
- Catch 会获取所有错误。
- 在
catch (err) {...}
块中,我们分析错误对象err
。 - 如果我们不知道如何处理它,我们执行
throw err
。
通常,我们可以使用 instanceof
运算符检查错误类型
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {
alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
}
}
我们还可以从 err.name
属性获取错误类名。所有本机错误都有它。另一个选项是读取 err.constructor.name
。
在下面的代码中,我们使用重新抛出,以便 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(); // unexpected error
alert( user.name );
} catch (err) {
if (err instanceof SyntaxError) {
alert( "JSON Error: " + err.message );
} else {
throw err; // rethrow (*)
}
}
在 catch
块内部的 (*)
行上抛出的错误“跳出”try...catch
,并且可以被外部 try...catch
结构(如果存在)捕获,或者它会终止脚本。
因此,catch
块实际上只处理它知道如何处理的错误,而“跳过”所有其他错误。
下面的示例演示了如何通过更多一级的 try...catch
捕获此类错误
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (err) {
// ...
if (!(err instanceof 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 (err) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
代码有两种执行方式
- 如果你对“制造错误?”回答“是”,则为
try -> catch -> finally
。 - 如果你说“否”,则为
try -> finally
。
当我们开始做某事并希望在任何情况下都完成它时,通常会使用 finally
子句。
例如,我们希望测量斐波那契数函数 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 (err) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "error occurred");
alert( `execution took ${diff}ms` );
你可以通过在 prompt
中输入 35
来检查代码的运行情况——它正常执行,finally
在 try
之后。然后输入 -1
——将立即出现错误,并且执行将花费 0ms
。两种测量都正确完成。
换句话说,函数可能以 return
或 throw
结束,这并不重要。finally
子句在这两种情况下都会执行。
try...catch...finally
中是局部的请注意,上面代码中的 result
和 diff
变量在 try...catch
之前声明。
否则,如果我们在 try
块中声明 let
,它将只能在其中可见。
finally
和 return
finally
子句适用于从 try...catch
的任何退出。其中包括显式的 return
。
在下面的示例中,try
中有一个 return
。在这种情况下,finally
在控制权返回到外部代码之前执行。
function func() {
try {
return 1;
} catch (err) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try...finally
没有 catch
子句的 try...finally
构造也很有用。当我们不想在此处处理错误(让它们贯穿)时,但希望确保我们启动的进程已完成时,我们应用它。
function func() {
// start doing something that needs completion (like measurements)
try {
// ...
} finally {
// complete that thing even if all dies
}
}
在上面的代码中,try
中的错误总是会抛出,因为没有 catch
。但在执行流离开函数之前,finally
会起作用。
全局捕获
本部分的信息不属于核心 JavaScript 的一部分。
让我们想象一下,我们在 try...catch
之外遇到了致命错误,并且脚本已死机。就像编程错误或其他可怕的事情。
有没有办法对这样的情况做出反应?我们可能希望记录错误,向用户显示一些内容(通常他们看不到错误消息)等。
规范中没有,但环境通常会提供它,因为它非常有用。例如,Node.js 为此提供了 process.on("uncaughtException")
。在浏览器中,我们可以将一个函数分配给特殊的 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 或 https://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
(非标准,但得到很好的支持) - 错误创建时的堆栈。
如果不需要错误对象,我们可以使用 catch {
代替 catch (err) {
来省略它。
我们还可以使用 throw
运算符生成我们自己的错误。从技术上讲,throw
的参数可以是任何内容,但通常它是一个继承自内置 Error
类的错误对象。有关在下一章中扩展错误的更多信息。
重新抛出是错误处理的一个非常重要的模式:catch
块通常会预期并知道如何处理特定错误类型,因此它应该重新抛出它不知道的错误。
即使我们没有 try...catch
,大多数环境也允许我们设置一个“全局”错误处理程序来捕获“掉出来”的错误。在浏览器中,那就是 window.onerror
。
评论
<code>
标记,对于多行 - 将它们包装在<pre>
标记中,对于 10 行以上 - 使用沙箱 (plnkr,jsbin,codepen…)