无论我们编程多么精通,脚本错误怎是难免。可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因。
通常,当发送错误时脚本会立刻停止,打印至控制台。
但
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)
}
alert(“…Then the execution continues”);
带错误示例:显示 (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 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引擎首先读代码,然后运行。发生在读阶段错误称为“解析时”错误,不可恢复,因为引擎不理解代码。
所以,try...catch
仅能处理有效代码中的错误,被称为“运行时”错误,有时也称为“异常”。
try..catch同步执行
如果在预定的(scheduled)代码发生异常,如setTimeout
,那么try...catch
不捕获异常:
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (e) {
alert( "won't work" );
}
因为try...catch
包装了setTimeout
调用预定函数。但函数会延后执行,此时引擎已经立刻了try...catch
结构。
为了捕获预定执行函数内的异常,try...catch
必须在函数内部:
setTimeout(function() {
try {
noSuchVariable; // try..catch handles the error!
} catch (e) {
alert( "error is caught here!" );
}
}, 1000);
当错误发生时,Javascript生成包含细节信息的对象,并作为参数传递给catch
块:
try {
// ...
} catch(err) { // <-- the "error object", could use another word instead of err
// ...
}
对所有内置错误,catch
内的错误对象主要有两个属性:
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”
让我们探索一个真实的用例:
我们知道,Javascript支持方法JSON.parse(str)
,用于读json值。通常用于解析从网络中接收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
非标准的,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 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (e) {
alert( "doesn't execute" );
}
这里 JSON.parse
执行正常,但缺少name属性,实际对我们来说是个错误。为了统一错误处理,我们需要使用throw
操作。
Throw操作
该操作产生一个错误。语法:
throw
技术上,可以使用任何内容作为错误对象。可以是原始类型,如数字或字符串,但最好使用对象,并带有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(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token o in JSON at position 0
}
如我们所见,错误为:SyntaxError
。
在我们的示例中,缺省name属性,也可以视为语法错误,假设users必须有个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
操作产生SyntaxError
错误,并带有给定的message
,与Javascript自身生成的错误一致。try块中的执行立刻停止,控制流跳至catch
块。
现在catch
变成了独立处理所有错误的块:JSON.parse
和其他错误。
再次抛出错误
上面的示例,我们使用try...catch
处理不正确的数据,但也可能是其他异常发生在try...catch
块中,如变量未定义或其他,不仅是“不正确的数据”。
如下:
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
// (not JSON Error actually)
}
当然,一切都是可能的!程序人员造成的错误,即使被大量使用的开源工具——可能会突然发现一个疯狂的bug,导致了可怕的黑客攻击(就像使用ssh工具发生的那样)。
在我们的示例中,try...catch
是为了处理不正确数据错误,但实际上,catch捕获try块中所有的错误。如有一个异常错误,仍然显示“JSON Error”消息,这样的错误也使代码更难调试。
幸运的是,我们能发现捕获的错误是那种类型,示例,从其name属性:
try {
user = { /*...*/ };
} catch(e) {
alert(e.name); // "ReferenceError" for accessing an undefined variable
}
规则很简单。
应该仅处理已知错误,重新抛出其他错误
详细的重新抛出错误解释如下:
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(); // unexpected error
alert( user.name );
} catch(e) {
if (e.name == "SyntaxError") {
alert( "JSON Error: " + e.message );
} else {
throw e; // rethrow (*)
}
}
在catch
块内部星号行抛出错误,其可以被外部的try...catch
结构块捕获(如果存在),或直接停止脚本。
所以catch
块实际上仅处理已知错误,并忽略所有其他错误。
下面示例演示这样的错误被多级try...catch
块处理。
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (e) {
// ...
if (e.name != 'SyntaxError') {
throw e; // rethrow (don't know how to deal with it)
}
}
}
try {
readData();
} catch (e) {
alert( "External catch got: " + e ); // caught it!
}
这里readData
仅知道如何处理SyntaxError错误,而外部的try...catch
知道如何处理任何错误。
try…catch…finally
等等,还没有完。
结构try...catch
可以有多个代码子句:finally
,如果存在,所有情况都会执行。
try
之后,如果没有错误情况catch
之后,如果有错误扩展语法类似如下:
try {
... try to execute the code ...
} catch(e) {
... handle errors ...
} finally {
... execute always ...
}
请尝试运行下面代码:
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
代码有两条执行路径:
try->catch->finally
.try->finally
.子句finally
通常应用场景为:在try...catch
块之前开始做某事,无论结果如何都需要终止。
举例,我们想衡量斐波拉切函数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,检查代码运行——执行正常,try之后执行finally。再次输入-1,立刻产生错误,执行花费0ms,正确完成时间测量。
换句话说,有两种方法可以退出函数:要么return,要么抛出错误。finally子句柄都会处理。
在try…catch…finally块中的变量是局部变量
注意在上面代码result
和diff
变量,是在try...catch
块之前声明的。
否则,如果使用let在{…}块里面,则只能在块内部可见。
finally 和 return
在try...catch
块无论如何结束,finally子句都执行。包括显示的return方式返回。
下面的示例,在try中有return,在这种情况,在控制返回外部代码之前,finally被执行。
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try…finally
try…finally结构,没有catch
子句,也有用。当我们不想在这里处理错误时可以应用,但是要确保开始和最终过程被执行。
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
块之外有个致命错误,那么代码会立刻停止。如编程错误或其他更糟糕的事情。
是否有应对此类事件的方法?我们可能想记录错误,向用户显示一些信息(通常他不会看到错误消息)等等。
Javascript规范中没有涉及,但环境通常都提供实现,因为确实有用。如,Node.JS有process.on(“uncaughtException”),在浏览器中可以给window.onerror赋值一个函数。它将在未捕获错误的情况下运行。
语法:
window.onerror = function(message, url, line, col, error) {
// ...
};
message
错误信息
url
发生错误脚本url
line, col
错误发生在代码中行、列数
error
错误对象
全局错误处理window.error
的角色通常不能恢复脚本执行,在编程错误的情况下是不可能的,但会给开发者发送错误信息。
也有一些web服务为这种情况提供了错误日志记录,如 https://errorception.com 或 http://www.muscula.com.
工作流程如下:
window.error
函数。结构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
的参数可以是任意类型,但通常是从内置错误类Error
继承的对象。后面会介绍扩展错误对象。
再次抛出是错误处理的基本模式:catch
块通常期望并知道怎样处理特定的错误,所以应该重新抛出未知错误。
即使我们没有使用try...catch
,大多数环境也支持设置全局的错误处理,捕获所有发生的错误,浏览器内置的是window.onerror
.