JavaScript--错误处理
一,错误处理,"try...catch"
1.1 语法
try { // 代码... } catch (err) { // 错误捕获 }它按照以下步骤执行:
- 首先,执行
try {...}中的代码。 - 如果这里没有错误,则忽略
catch (err):执行到try的末尾并跳过catch继续执行。 - 如果这里出现错误,则
try执行停止,控制流转向catch (err)的开头。变量err(我们可以使用任何名称)将包含一个 error 对象,该对象包含了所发生事件的详细信息。
try...catch同步执行
如果在“计划的(scheduled)”代码中发生异常,例如在setTimeout中,则try...catch不会捕获到异常:
try { setTimeout(function() { noSuchVariable; // 脚本将在这里停止运行 }, 1000); } catch (err) { alert( "不工作" ); }因为try...catch包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了try...catch结构。
为了捕获到计划的(scheduled)函数中的异常,那么try...catch必须在这个函数内:
setTimeout(function() { try { noSuchVariable; // try...catch 处理 error 了! } catch { alert( "error 被在这里捕获了!" ); } }, 1000);1.2error对象
发生错误时,JavaScript 会生成一个包含有关此 error 详细信息的对象。然后将该对象作为参数传递给catch
对于所有内建的 error,error 对象具有两个主要属性:
name
Error 名称。例如,对于一个未定义的变量,名称是"ReferenceError"。
message
关于 error 的详细文字描述。
还有其他非标准的属性在大多数环境中可用。其中被最广泛使用和支持的是:
stack
当前的调用栈:用于调试目的的一个字符串,其中包含有关导致 error 的嵌套调用序列的信息。
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) // 也可以将一个 error 作为整体显示出来 // error 信息被转换为像 "name: message" 这样的字符串 alert(err); // ReferenceError: lalala is not defined }1.3 使用
让我们一起探究一下真实场景中try...catch的用例。
正如我们所知道的,JavaScript 支持JSON.parse(str)方法来解析 JSON 编码的值。
通常,它被用来解析从网络、服务器或是其他来源接收到的数据。
如果json格式错误,JSON.parse就会生成一个 error,因此脚本就会“死亡”。
我们对此满意吗?当然不!
如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)。代码执行失败却没有提示信息,这真的是很糟糕的用户体验。
让我们用try...catch来处理这个 error:
letjson ="{ bad json }";try{letuser =JSON.parse(json);// <-- 当出现 error 时...alert( user.name);// 不工作}catch(err) {// ...执行会跳转到这里并继续执行alert("很抱歉,数据有错误,我们会尝试再请求一次。");alert( err.name);alert( err.message); }1.4throw操作符
如果这个json在语法上是正确的,但是没有所必须的name属性该怎么办?
letjson ='{ "age": 30 }';// 不完整的数据try{letuser =JSON.parse(json);// <-- 没有 erroralert( user.name);// 没有 name!}catch(err) {alert("doesn't execute"); }这里JSON.parse正常执行,但缺少name属性对我们来说确实是个 error。
为了统一进行 error 处理,我们将使用throw操作符。
throw的语法
throw<errorobject>技术上讲,我们可以将任何东西用作 error 对象。甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有name和message属性的对象(某种程度上保持与内建 error 的兼容性)。
JavaScript 中有很多内建的标准 error 的构造器:Error,SyntaxError,ReferenceError,TypeError等。我们也可以使用它们来创建 error 对象。
leterror =newError(message);// 或leterror =newSyntaxError(message);leterror =newReferenceError(message);// ...让我们来看看JSON.parse会生成什么样的 error:
try{JSON.parse("{ bad json o_O }"); }catch(err) {alert(err.name);// SyntaxErroralert(err.message);// Unexpected token b in JSON at position 2}正如我们所看到的, 那是一个SyntaxError。
在我们的示例中,缺少name属性就是一个 error,因为用户必须有一个name。
所以,让我们抛出这个 error。
letjson ='{ "age": 30 }';// 不完整的数据try{letuser =JSON.parse(json);// <-- 没有 errorif(!user.name) {thrownewSyntaxError("数据不全:没有 name");// (*)}alert( user.name); }catch(err) {alert("JSON Error: "+ err.message);// JSON Error: 数据不全:没有 name}1.5 再次抛出
catch 会捕获到所有来自于try的 error。在这儿,它捕获到了一个预料之外的 error,但仍然抛出的是同样的错误 信息。这是不正确的,并且也会使代码变得更难以调试。
catch应该只处理它知道的 error,并“抛出”所有其他 error。
“再次抛出(rethrowing)”技术可以被更详细地解释为:
- Catch 捕获所有 error。
- 在
catch (err) {...}块中,我们对 error 对象err进行分析。 - 如果我们不知道如何处理它,那我们就
throw err。
通常,我们可以使用instanceof操作符判断错误类型:
try{ user = {/*...*/}; }catch(err) {if(errinstanceofReferenceError) {alert('ReferenceError');// 访问一个未定义(undefined)的变量产生了 "ReferenceError"} }letjson ='{ "age": 30 }';// 不完整的数据try{letuser =JSON.parse(json);if(!user.name) {thrownewSyntaxError("数据不全:没有 name"); }blabla();// 预料之外的 erroralert( user.name); }catch(err) {if(errinstanceofSyntaxError) {alert("JSON Error: "+ err.message); }else{throwerr;// 再次抛出 (*)} }调用
readData(触发内部
执行到try块中的错误blabla();时,因为blabla这个函数在全局作用域中根本不存在,JavaScript 引擎会立刻抛出ReferenceError(引用错误)。try块内的代码执行就此中断,直接跳转到内部的catch块。进入内部
此时捕获到的catch (err)块进行错误筛查err是ReferenceError。代码执行判断:if (!(err instanceof SyntaxError))内部错误“重新抛出”(Rethrow)
因为判断条件为真,
if内部的throw err;被执行。内部的catch觉得自己没法处理这个ReferenceError(它只认识SyntaxError),于是把这个错误原封不动地向上抛,readData()函数执行被强制终止,并把这个错误传到了外面。
外部的
代码回到全局作用域,外层的try...catch接盘try { readData(); }检测到了从内部抛出的ReferenceError。外层的catch (err)成功捕获到这个错误。最终执行结果
执行外层的
alert( "External catch got: " + err );。浏览器弹出警告框,显示捕获到的错误信息。
catch只应该处理你明确知道怎么处理的错误。如果遇到你无法识别或无法解决的错误,绝对不要对它保持沉默(不要写空的catch块什么都不做),而应该使用throw err把它抛出去,让调用该函数的外部代码或者全局错误监听器来处理。这能防止程序“静默失败”,避免留下难以排查的 Bug。
1.6 try.....catch........finally
try { ... 尝试执行的代码 ... } catch (err) { ... 处理 error ... } finally { ... 总是会执行的代码 ... }let num = +prompt("输入一个正整数?", 35) let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("不能是负数,并且必须是整数。"); } 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"); alert( `执行花费了 ${diff}ms` );finally子句适用于try...catch的任何出口。这包括显式的return。
二。自定义 Error,扩展 Error
2.1拓展Error
Error类是内建的,但我们可以通过下面这段近似代码理解我们要扩展的内容:
// JavaScript 自身定义的内建的 Error 类的“伪代码” class Error { constructor(message) { this.message = message; this.name = "Error"; // (不同的内建 error 类有不同的名字) this.stack = <call stack>; // 非标准的,但大多数环境都支持它 } }现在让我们从其中继承ValidationError试一试:
class ValidationError extends Error { constructor(message) { super(message); // (1) this.name = "ValidationError"; // (2) } } function test() { throw new ValidationError("Whoops!"); } try { test(); } catch(err) { alert(err.message); // Whoops! alert(err.name); // ValidationError alert(err.stack); // 一个嵌套调用的列表,每个调用都有对应的行号 }class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // 用法 function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("No field: age"); } if (!user.name) { throw new ValidationError("No field: name"); } return user; } // try..catch 的工作示例 try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // Invalid data: No field: name } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); } else { throw err; // 未知的 error,再次抛出 (**) } }我们也可以看看err.name,像这样:
// ... // instead of (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...使用instanceof的版本要好得多,因为将来我们会对ValidationError进行扩展,创建它的子类型,例如PropertyRequiredError。而instanceof检查对于新的继承类也适用。所以这是面向未来的做法。
深入继承
ValidationError类是非常通用的。很多东西都可能出错。对象的属性可能缺失或者属性可能有格式错误(例如age属性的值为一个字符串而不是数字)。
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); this.name = "PropertyRequiredError"; this.property = property; } } // 用法 function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } return user; } // try..catch 的工作示例 try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // Invalid data: No property: name alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("JSON Syntax Error: " + err.message); } else { throw err; // 未知 error,将其再次抛出 } }在PropertyRequiredErrorconstructor 中的this.name是通过手动重新赋值的。这可能会变得有些乏味 —— 在每个自定义 error 类中都要进行this.name = <class name>赋值操作。我们可以通过创建自己的“基础错误(basic error)”类来避免这种情况,该类进行了this.name = this.constructor.name赋值。然后让所有我们自定义的 error 都从这个“基础错误”类进行继承。
让我们称之为MyError。
class MyError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); this.property = property; } } // name 是对的 alert( new PropertyRequiredError("field").name ); // PropertyRequiredError2.2包装异常
将来,函数readUser可能会不断壮大,并可能会产生其他类型的 error。
我们是否真的想每次都一一检查所有的 error 类型?
通常答案是 “No”:我们希望能够“比它高一个级别”。我们只想知道这里是否是“数据读取异常” —— 为什么发生了这样的 error 通常是无关紧要的
我们所描述的这项技术被称为“包装异常”。
- 我们将创建一个新的类
ReadError来表示一般的“数据读取” error。 - 函数
readUser将捕获内部发生的数据读取 error,例如ValidationError和SyntaxError,并生成一个ReadError来进行替代。 - 对象
ReadError会把对原始 error 的引用保存在其cause属性中。
class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { if (err instanceof SyntaxError) { throw new ReadError("Syntax Error", err); } else { throw err; } } try { validateUser(user); } catch (err) { if (err instanceof ValidationError) { throw new ReadError("Validation Error", err); } else { throw err; } } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { alert(e); // Original error: SyntaxError: Unexpected token b in JSON at position 1 alert("Original error: " + e.cause); } else { throw e; } }