错误处理try...catch
概述
- 通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来
- 但是有一种语法结构
try...catch,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉
语法
- 首先,执行
try {...}中的代码 - 如果这里没有错误,则忽略
catch (err):执行到try的末尾并跳过catch继续执行 - 如果这里出现错误,则
try执行停止,控制流转向catch (err)的开头。变量err(我们可以使用任何名称)将包含一个error对象,该对象包含了所发生事件的详细信息
|
1 2 3 4 5 6 7 8 9 |
try { // 代码... } catch (err) { // 错误捕获 } |
示例
- 所以,
try {...}块内的 error 不会杀死脚本 —— 我们有机会在catch中处理它 - 没有
error的例子:显示alert(1)和(2):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { alert('开始执行 try 中的内容'); // (1) <-- // ...这里没有 error alert('try 中的内容执行完毕'); // (2) <-- } catch (err) { alert('catch 被忽略,因为没有 error'); // (3) } |
- 包含
error的例子:显示(1)和(3)行的alert中的内容:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { alert('开始执行 try 中的内容'); // (1) <-- lalala; // error,变量未定义! alert('try 的末尾(未执行到此处)'); // (2) } catch (err) { alert(`出现了 error!`); // (3) <-- } |
注意
try...catch仅对运行时的error有效- 要使得
try...catch能工作,代码必须是可执行的 - 换句话说,它必须是有效的
JavaScript代码 - 如果代码包含语法错误,那么
try..catch将无法正常工作,例如含有不匹配的花括号: JavaScript引擎首先会读取代码,然后运行它。
在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)
这是因为引擎无法理解该代码- 所以,
try...catch只能处理有效代码中出现的错误
这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”
- 要使得
|
1 2 3 4 5 |
try { {{{{{{{{{{{{ } catch (err) { alert("引擎无法理解这段代码,它是无效的"); } |
try...catch同步执行- 如果在“计划的(
scheduled)”代码中发生异常,例如在setTimeout中,则try...catch不会捕获到异常: - 因为
try...catch包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了try...catch结构
- 如果在“计划的(
|
1 2 3 4 5 6 7 |
try { setTimeout(function() { noSuchVariable; // 脚本将在这里停止运行 }, 1000); } catch (err) { alert( "不工作" ); } |
- 为了捕获到计划的(
scheduled)函数中的异常,那么try...catch必须在这个函数内:
|
1 2 3 4 5 6 7 |
setTimeout(function() { try { noSuchVariable; // try...catch 处理 error 了! } catch { alert( "error 被在这里捕获了!" ); } }, 1000); |
Error对象
- 发生错误时,
JavaScript会生成一个包含有关此error详细信息的对象。然后将该对象作为参数传递给catch:
|
1 2 3 4 5 |
try { // ... } catch (err) { // <-- “error 对象”,也可以用其他参数名代替 err // ... } |
- 对于所有内建的
error,error对象具有两个主要属性:nameError名称
例如,对于一个未定义的变量,名称是"ReferenceError"message关于error的详细文字描述- 还有其他非标准的属性在大多数环境中可用。其中被最广泛使用和支持的是:
stack当前的调用栈:
用于调试目的的一个字符串,其中包含有关导致error的嵌套调用序列的信息
|
1 2 3 4 5 6 7 8 9 10 11 |
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 } |
可选的 “catch” 绑定
- 这是一个最近添加到
JavaScript的特性 - 如果我们不需要 error 的详细信息,
catch也可以忽略它:
|
1 2 3 4 5 |
try { // ... } catch { // <-- 没有 (err) // ... } |
使用 “try…catch”
JavaScript支持JSON.parse(str)方法来解析JSON编码的值- 通常,它被用来解析从网络、服务器或是其他来源接收到的数据
- 像下面这样调用
JSON.parse:
- 像下面这样调用
|
1 2 3 4 5 6 7 |
let json = '{"name":"John", "age": 30}'; // 来自服务器的数据 let user = JSON.parse(json); // 将文本表示转换成 JavaScript 对象 // 现在 user 是一个解析自 json 字符串的有自己属性的对象 alert( user.name ); // John alert( user.age ); // 30 |
- 如果
json格式错误,JSON.parse就会生成一个error,因此脚本就会“死亡”- 我们对此满意吗?当然不!
- 如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)
- 代码执行失败却没有提示信息,这真的是很糟糕的用户体验
- 在这儿,我们将
catch块仅仅用于显示信息
但我们可以做更多的事:发送一个新的网络请求,向访问者建议一个替代方案,将有关错误的信息发送给记录日志的设备
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- 当出现 error 时... alert( user.name ); // 不工作 } catch (err) { // ...执行会跳转到这里并继续执行 alert( "很抱歉,数据有错误,我们会尝试再请求一次。" ); alert( err.name ); alert( err.message ); } |
抛出我们自定义的 error
- 如果这个
json在语法上是正确的,但是没有所必须的name属性该怎么办?- 这里
JSON.parse正常执行,但缺少name属性对我们来说确实是个error
- 这里
|
1 2 3 4 5 6 7 8 9 10 |
let json = '{ "age": 30 }'; // 不完整的数据 try { let user = JSON.parse(json); // <-- 没有 error alert( user.name ); // 没有 name! } catch (err) { alert( "doesn't execute" ); } |
- 为了统一进行
error处理,我们将使用throw操作符
throw操作符
throw操作符会生成一个error对象- 技术上讲,我们可以将任何东西用作
error对象 - 甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有
name和message属性的对象(某种程度上保持与内建error的兼容性)
- 技术上讲,我们可以将任何东西用作
|
1 |
throw <error object> |
JavaScript中有很多内建的标准error的构造器:Error,SyntaxError,ReferenceError,TypeError等- 我们也可以使用它们来创建
error对象
- 我们也可以使用它们来创建
|
1 2 3 4 5 |
let error = new Error(message); // 或 let error = new SyntaxError(message); let error = new ReferenceError(message); // ... |
- 对于内建的
error(不是对于其他任何对象,仅仅是对于error),name属性刚好就是构造器的名字。message则来自于参数(argument)
|
1 2 3 4 |
let error = new Error("Things happen o_O"); alert(error.name); // Error alert(error.message); // Things happen o_O |
- 看看
JSON.parse会生成什么样的error- 在
(*)标记的这一行,throw操作符生成了包含着我们所给定的message的SyntaxError,与JavaScript自己生成的方式相同 try的执行立即停止,控制流转向catch块
- 在
|
1 2 3 4 5 6 |
try { JSON.parse("{ bad json o_O }"); } catch(err) { alert(err.name); // SyntaxError alert(err.message); // Unexpected token b in JSON at position 2 } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let json = '{ "age": 30 }'; // 不完整的数据 try { let user = JSON.parse(json); // <-- 没有 error if (!user.name) { throw new SyntaxError("数据不全:没有 name"); // (*) } alert( user.name ); } catch(err) { alert( "JSON Error: " + err.message ); // JSON Error: 数据不全:没有 name } |
再次抛出Rethrowing
- 上面的例子中,我们使用
try...catch来处理不正确的数据 - 但是在
try {...}块中是否可能发生 另一个预料之外的error?- 例如编程错误(未定义变量)或其他错误,而不仅仅是这种“不正确的数据”
- 在这儿,它捕获到了一个预料之外的
error,但仍然抛出的是同样的"JSON Error"信息。这是不正确的,并且也会使代码变得更难以调试
|
1 2 3 4 5 6 7 8 9 10 |
let json = '{ "age": 30 }'; // 不完整的数据 try { user = JSON.parse(json); // <-- 忘记在 user 前放置 "let" // ... } catch (err) { alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined // (实际上并没有 JSON Error) } |
- 为了避免此类问题,我们可以采用“重新抛出”技术。规则很简单:
catch应该只处理它知道的error,并“抛出”所有其他error- 更详细地解释为:
Catch捕获所有error- 在
catch (err) {...}块中,我们对error对象err进行分析 - 如果我们不知道如何处理它,那我们就
throw err
|
1 2 3 4 5 6 7 |
try { user = { /*...*/ }; } catch (err) { if (err instanceof ReferenceError) { alert('ReferenceError'); // 访问一个未定义(undefined)的变量产生了 "ReferenceError" } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
let json = '{ "age": 30 }'; // 不完整的数据 try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("数据不全:没有 name"); } blabla(); // 预料之外的 error alert( user.name ); } catch (err) { if (err instanceof SyntaxError) { alert( "JSON Error: " + err.message ); } else { throw err; // 再次抛出 (*) } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function readData() { let json = '{ "age": 30 }'; try { // ... blabla(); // error! } catch (err) { // ... if (!(err instanceof SyntaxError)) { throw err; // 再次抛出(不知道如何处理它) } } } try { readData(); } catch (err) { alert( "External catch got: " + err ); // 捕获了它! } |
try…catch…finally
try...catch结构可能还有一个代码子句(clause):finally- 如果它存在,它在所有情况下都会被执行:
try之后,如果没有error,catch之后,如果有error
|
1 2 3 4 5 6 7 |
try { ... 尝试执行的代码 ... } catch (err) { ... 处理 error ... } finally { ... 总是会执行的代码 ... } |
- 示例代码:
- 对于 “
Make an error?” 的回答是 “Yes”,那么执行try -> catch -> finally - 如果你的回答是 “
No”,那么执行try -> finally
- 对于 “
|
1 2 3 4 5 6 7 8 |
try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); } catch (err) { alert( 'catch' ); } finally { alert( 'finally' ); } |
finally子句(clause)通常用在:当我们开始做某事的时候,希望无论出现什么情况都要完成某个任务- 代码中的
result和diff变量都是在try...catch之前 声明的 finally子句适用于try...catch的 任何 出口。这包括显式的return- 没有
catch子句的try...finally结构也很有用。当我们不想在原地处理error(让它们掉出去吧),但是需要确保我们启动的处理需要被完成时,我们应当使用它
- 代码中的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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` ); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function func() { try { return 1; } catch (err) { /* ... */ } finally { alert( 'finally' ); } } alert( func() ); // 先执行 finally 中的 alert,然后执行这个 alert |
|
1 2 3 4 5 6 7 8 |
function func() { // 开始执行需要被完成的操作(比如测量) try { // ... } finally { // 完成前面我们需要完成的那件事,即使 try 中的执行失败了 } } |
全局 catch
- 这个部分的内容并不是
JavaScript核心的一部分 - 设想一下,在
try...catch结构外有一个致命的error,然后脚本死亡了- 有什么办法可以用来应对这种情况吗?我们可能想要记录这个
error,并向用户显示某些内容(通常用户看不到错误信息)等 - 规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用
- 例如,
Node.JS有process.on("uncaughtException") - 在浏览器中,我们可以将一个函数赋值给特殊的
window.onerror属性,该函数将在发生未捕获的error时执行
- 有什么办法可以用来应对这种情况吗?我们可能想要记录这个
- 语法
messageerror信息url发生error的脚本的URLline,col发生error处的代码的行号和列号errorerror对象
|
1 2 3 |
window.onerror = function(message, url, line, col, error) { // ... }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
<script> window.onerror = function(message, url, line, col, error) { alert(`${message}\n At ${line}:${col} of ${url}`); }; function readData() { badFunc(); // 啊,出问题了! } readData(); </script> |
- 全局错误处理程序
window.onerror的作用通常不是恢复脚本的执行 —— 如果发生编程错误,恢复脚本的执行几乎是不可能的,它的作用是将错误信息发送给开发者
自定义 Error,扩展 Error
概述
- 当我们在开发某些东西时,经常会需要我们自己的
error类来反映在我们的任务中可能出错的特定任务 - 对于网络操作中的
error,我们需要HttpError,对于数据库操作中的error,我们需要DbError,对于搜索操作中的error,我们需要NotFoundError,等等 - 我们自定义的
error应该支持基本的error的属性- 例如
message,name,并且最好还有stack - 但是它们也可能会有其他属于它们自己的属性,例如,
HttpError对象可能会有一个statusCode属性,属性值可能为404、403或500等
- 例如
JavaScript允许将throw与任何参数一起使用,所以从技术上讲,我们自定义的error不需要从Error中继承- 但是,如果我们继承,那么就可以使用
obj instanceof Error来识别error对象。因此,最好继承它 - 随着开发的应用程序的增长,我们自己的
error自然会形成形成一个层次结构(hierarchy)。例如,HttpTimeoutError可能继承自HttpError,等等
- 但是,如果我们继承,那么就可以使用
扩展Error
- 例如,让我们考虑一个函数
readUser(json),该函数应该读取带有用户数据的JSON- 在函数内部,我们将使用
JSON.parse。如果它接收到格式不正确的json,就会抛出SyntaxError - 但是,即使
json在语法上是正确的,也不意味着该数据是有效的用户数据,对吧? - 因为它可能丢失了某些必要的数据。例如,对用户来说,必不可少的是
name和age属性
- 在函数内部,我们将使用
|
1 |
let json = `{ "name": "John", "age": 30 }`; |
- 我们的函数
readUser(json)不仅会读取JSON,还会检查(“验证”)数据。如果没有所必须的字段,或者(字段的)格式错误,那么就会出现一个error- 并且这些并不是
SyntaxError,因为这些数据在语法上是正确的,这些是另一种错误 - 我们称之为
ValidationError,并为之创建一个类 - 这种类型的错误也应该包含有关违规字段的信息
- 我们的
ValidationError类应该继承自Error类 Error类是内建的,但我们可以通过下面这段近似代码理解我们要扩展的内容:
- 并且这些并不是
|
1 2 3 4 5 6 7 8 |
// JavaScript 自身定义的内建的 Error 类的“伪代码” class Error { constructor(message) { this.message = message; this.name = "Error"; // (不同的内建 error 类有不同的名字) this.stack = <call stack>; // 非标准的,但大多数环境都支持它 } } |
- 从其中继承
ValidationError试一试:- 在
(1)行中我们调用了父类的constructor。JavaScript要求我们在子类的constructor中调用super,所以这是必须的。父类的constructor设置了message属性 - 父类的
constructor还将name属性的值设置为了"Error",所以在(2)行中,我们将其重置为了右边的值
- 在
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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); // 一个嵌套调用的列表,每个调用都有对应的行号 } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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,像这样:- 使用
instanceof的版本要好得多,因为将来我们会对ValidationError进行扩展,创建它的子类型 - 例如
PropertyRequiredError。而instanceof检查对于新的继承类也适用。所以这是面向未来的做法
- 使用
|
1 2 3 4 |
// ... // instead of (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ... |
深入继承
ValidationError类是非常通用的。很多东西都可能出错- 对象的属性可能缺失或者属性可能有格式错误(例如
age属性的值为一个字符串而不是数字) - 我们针对缺少属性的错误来制作一个更具体的
PropertyRequiredError类 - 这个新的类
PropertyRequiredError使用起来很简单:我们只需要传递属性名:new PropertyRequiredError(property)。人类可读的message是由constructor生成的
- 对象的属性可能缺失或者属性可能有格式错误(例如
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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,将其再次抛出 } } |
MyError
- 注意,在
PropertyRequiredErrorconstructor中的this.name是通过手动重新赋值的- 这可能会变得有些乏味 —— 在每个自定义
error类中都要进行this.name = <class name>赋值操作 - 我们可以通过创建自己的“基础错误(
basic error)”类来避免这种情况,该类进行了this.name = this.constructor.name赋值 - 然后让所有我们自定义的
error都从这个“基础错误”类进行继承
- 这可能会变得有些乏味 —— 在每个自定义
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 ); // PropertyRequiredError |
包装异常
- 上面代码中的函数
readUser的目的就是“读取用户数据”。在这个过程中可能会出现不同类型的error - 目前我们有了
SyntaxError和ValidationError,但是将来,函数readUser可能会不断壮大,并可能会产生其他类型的error - 调用
readUser的代码应该处理这些error。现在它在catch块中使用了多个if语句来检查error类,处理已知的error,并再次抛出未知的error- 如果
readUser函数会产生多种error,那么我们应该问问自己:我们是否真的想每次都一一检查所有的error类型? - 通常答案是 “
No”:我们希望能够“比它高一个级别”。我们只想知道这里是否是“数据读取异常” —— 为什么发生了这样的error通常是无关紧要的 - 或者,如果我们有一种方式能够获取
error的详细信息那就更好了,但前提是我们需要 - 我们所描述的这项技术被称为“包装异常”
- 如果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { ... readUser() // 潜在的 error 源 ... } catch (err) { if (err instanceof ValidationError) { // 处理 validation error } else if (err instanceof SyntaxError) { // 处理 syntax error } else { throw err; // 未知 error,再次抛出它 } } |
- 包装异常
- 我们将创建一个新的类
ReadError来表示一般的“数据读取”error - 函数
readUser将捕获内部发生的数据读取error,例如ValidationError和SyntaxError,并生成一个ReadError来进行替代 - 对象
ReadError会把对原始error的引用保存在其cause属性中 - 之后,调用
readUser的代码只需要检查ReadError,而不必检查每种数据读取error。并且,如果需要更多error细节,那么可以检查readUser的cause属性
- 我们将创建一个新的类
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
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; } } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!