错误处理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
对象具有两个主要属性:name
Error
名称
例如,对于一个未定义的变量,名称是"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
时执行
- 有什么办法可以用来应对这种情况吗?我们可能想要记录这个
- 语法
message
error
信息url
发生error
的脚本的URL
line
,col
发生error
处的代码的行号和列号error
error
对象
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
- 注意,在
PropertyRequiredError
constructor
中的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所有,欢迎分享本文,转载请保留出处!