无论我们的编程能力有多强,有时我们的脚本都会有错误。它们可能是由于我们的错误、意外的用户输入、错误的服务器响应以及其他一千个原因造成的。
通常,脚本在发生错误时会“死亡”(立即停止),并将其打印到控制台。
但有一个语法结构 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
。