【Web技术】286- 自定义错误及扩展错误
英文:Ilya Kantor 译文:LeviDing
https://zh.javascript.info/custom-errors
当我们在进行开发的时候,通常需要属于我们自己的错误类来反映任务中可能出现的特殊情况。对于网络操作错误,我们需要 HttpError,对于数据库操作错误,我们需要 DbError,对于搜索操作错误,我们需要 NotFoundError,等等。
我们自定义的错误应该具有基本的错误属性,例如 message,name 以及更加详细的 stack。但是它们也会有属于自己的属性。举个例子,HttpError 对象会有一个 statusCode 属性,取值可能为 404、403 或 500 等。
JavaScript 允许我们在使用 throw 时带任何参数,所以从技术层面上说,我们自定义的错误不需要继承 Error 类,但如果我们继承了这个类,就能使用 obj instanceof Error 来鉴别错误对象,所以我们最好继承它。
在我们进行开发时,我们自己的异常类通常是有层次结构的,例如 HttpTimeoutError 可能继承自 HttpError 等。
扩展错误
让我们用一个能够读取用户数据的函数 readUser(json) 来作为例子。
这里是一个可用的 json 的例子:
let json = `{ "name": "John", "age": 30 }`;
在这里面,我们使用 JSON.parse。如果它接收到错误的 json,就会抛出 SyntaxError。
但即使是格式正确的 json,也并不表示它就是可用的,对吧?它有可能会遗漏一些必要的数据。例如,缺失了对用户所必需的 name 和 age 属性。
函数 readUser(json) 不仅会读取 JSON,也会检查(验证)数据。如果没有所需要的字段,或者格式不正确,那也是错误。而这不是 SyntaxError,因为数据在语法上是正确的,但是有其他的错误。我们称之为 ValidationError 并且为之创建一个类。这种类型的错误也应该承载缺少的字段的信息。
我们的 ValidationError 类应该继承自内置的 Error 类。
Error 类是内置的,但是我们需要看一下大致的代码,来理解我们需要扩展什么。
代码如下:
// 由JavaScript本身定义的内置错误类“伪代码”class Error { constructor(message) { this.message = message; this.name = "Error"; //(不同内置错误类别的名称) this.stack = <nested calls>; // 非标准,但大多数环境支持它
}
}
现在让我们开始用 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); // 验证错误 alert(err.stack); // 每个行编号的嵌套调用列表}
来看看构造器:
行 (1) 被称为父类构造器。JavaScript 需要我们在子类构造器中调用 super,这是强制性的。父类构造器设定 message 属性。
父类构造器也设定 name 的值为 “Error”,所以在行 (2) 我们将其重置为正确的值
让我们用 readUser(json) 来试试:
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; }}// Usagefunction 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); // 无效的数据:缺失字段:name } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); } else { throw err; // 未知错误,再次抛出(**) }}
上面的 try..catch 代码块同时处理我们的 ValidationError 和来自 JSON.parse 的内置 SyntaxError。
接下来看看我们是如何使用 instanceof 来检测行 (*) 中的特定错误类型。
也看看 err.name,就像这样:
// ...// instead of (err instanceof SyntaxError)} else if (err.name == "SyntaxError") { // (*)// ...
使用 instanceof 的做法会好很多,因为我们在以后会扩展 ValidationError,创造一个它的子类型,例如 PropertyRequiredError。而 instanceof 对于新的继承类也适用。所以这是个长远的保证。
还有一点很重要,在 catch 语句捕捉到未知的错误时,它会在抛出行 (**) 处重新抛出,catch 语句仅仅知道如何处理验证和语法错误,而其他错误(代码中的打印错误等)不应该被捕获。
更进一步的继承
ValidationError 类是十分通用的。因此可能会在某些方面出错。属性可能缺失,格式可能发生错误(例如 age 属性的值为一个字符串)。让我们来创造一个更加具体的类 PropertyRequiredError,为属性缺失的错误而量身定做的。它将会承载属性缺失的相关信息。
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; }}// Usagefunction 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); // 无效的数据:缺失属性:name alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("JSON Syntax Error: " + err.message); } else { throw err; // 未知错误,再次抛出 }}
这个 PropertyRequiredError 十分容易上手:我们只需要传递属性名:new PropertyRequiredError(property)。易懂的 message 属性将会由构造器提供。
需要注意的是,在 PropertyRequiredError 构造器中的 this.name 是再次进行手动赋值的。这可能会造成冗余 —— 在创建每个自定义错误的时候都要进行赋值 this.name = <class name>。但这并不是唯一的办法。我们可以创建自己的“基础异常”类,通过将 this.constructor.name 赋值给 this.name 来卸下我们肩上的负担,然后再进行继承。
我们称其为 MyError。
这是 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 is correctalert( new PropertyRequiredError("field").name ); // PropertyRequiredError
现在的自定义错误更加的简洁,特别是 ValidationError,我们在其构造器中删除了 “this.name = …” 这一行。
包装异常
上述代码中的函数 readUser 的目的就是“读取用户数据”,对吧?在此过程中可能会出现多个不同类型的异常,目前我们有 SyntaxError 和 ValidationError,但在将来,函数 readUser 将会不断壮大,新添加的代码或许会导致其他类型的异常。
调用函数 readUser 的代码要能够处理这些异常。现在它在 catch 语句块中使用多个 if 语句来检测不同类型的异常以及抛出未知异常。但如果函数 readUser 抛出了多种异常 —— 我们扪心自问:我们真的需要一个接一个地处理它抛出的异常吗?
通常答案是 “No”:外部代码想要比其他代码更高一级。它想要一些类似于“数据读取异常“的东西。它为什么发生 —— (其错误描述信息)通常是不相关的。或者,如果能有一种获取异常细节的办法就更好了,但这仅限于我们需要的时候。
所以,我们创建一个 ReadError 类来表现上述的异常。如果在函数 readUser 中发生了异常,我们会将其捕获,并生成 ReadError。我们同时也会在其 cause 属性中保留对原始异常的引用。那么外部的代码就只需要检测 ReadError。
下面的代码定义了 ReadError ,并演示了如何在 readUser 和 try..catch 中使用它:
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); // 原错误:语法错误:在位置 1 处不应有 b alert("Original error: " + e.cause); } else { throw e; }}
上述代码中,readUser 正如描述的一样正常工作 —— 捕获语法以及验证的异常并且抛出 ReadError 异常用来代替之前的行为(未知的异常依旧重新抛出)。
所以外部代码负责检测 instanceof ReadError,不必列出所有可能的异常类型。
这种途径称为“包装异常”,因为我们将“低级别的异常”包装为 ReadError,使得调用代码更加抽象和方便。它在面向对象编程中被广泛使用。
总结
我们能够正常地继承 Error 以及其他内置的错误类,只需要注意 name 属性以及不要忘了调用 super。
大多数时候,我们应该使用 instanceof 来检测一些特定的异常。它也能够在继承中使用。但有时我们会发现来自第三方库的异常,并且不容易得到它的类。那么 name 属性就可用于这一类的检测。
包装异常是一种广泛应用的技术,当一个函数处理低级别的异常时,用一个高级别的对象来报告错误。低级别的异常有时会变成这个对象的属性,就像上面例子中的 err.cause,但这并不严格要求。
每一个“在看”,都是对我最大的肯定!
【Web技术】286- 自定义错误及扩展错误的更多相关文章
- web.xml文件中的7个错误的安全配置
web.xml文件中的7个错误的安全配置 关于Java的web.xml文件中配置认证和授权有大 量 的 文章.本文不再去重新讲解如何配置角色.保护web资源和设置不同类型的认证,让我们来看看web.x ...
- 【转载】ASP.NET自定义404和500错误页面
在ASP.NET网站项目实际上线运行的过程中,有时候在运行环境下会出现400错误或者500错误,这些错误默认的页面都不友好,比较简单单调,其实我们可以自行设置这些错误所对应的页面,让这些错误跳转到我们 ...
- Web开发中的18个关键性错误
前几年,我有机会能参与一些有趣的项目,并且独立完成开发.升级.重构以及新功能的开发等工作. 本文总结了一些PHP程序员在Web开发中经常 忽略的关键错误,尤其是在处理中大型的项目上问题更为突出.典型的 ...
- 无法在Web服务器上启动调试,与Web服务器通信时出现身份验证错误
问题描述: 我使用的是修改hosts,模拟真实网址来进行调试的.具体是这样的:我修改hosts文件,把某个域名,如www.163.com映射为127.0.0.1,然后在IIS信息管理器中,创建一个网站 ...
- (转)Web开发中最致命的小错误
Web开发中最致命的小错误 现在,有越来越多所谓的“教程”来帮助我们提高网站的易用性.本文收集了一些在 Web 开发中容易出错和被忽略的小问题,并且提供了参考的解决方案,以便于帮助 Web 开发者更好 ...
- 将web项目导入到eclipse中常见错误
将web项目导入到eclipse中常见错误 错误1:string cannot be resolved to a type 原因:这种情况一般是因为你的JDK版本没有设置好,或者设置的有不一致的 ...
- Struts2自定义Field级别的错误提示信息
自定义Field级别的错误提示信息步骤: 在action包中新建一个以Action命名的properties文件,如:RegisterAction.properties 2. 然后在该属性文件中指定每 ...
- ubuntu或者debian安装php-gd扩展错误
‘./configure’ ‘–prefix=/usr/local/php’ ‘–with-config-file-path=/etc’ ‘–with-mysql=/usr/local/mysql’ ...
- 深入分析Java Web技术内幕(修订版)
阿里巴巴集团技术丛书 深入分析Java Web技术内幕(修订版)(阿里巴巴集团技术丛书.技术大牛范禹.玉伯.毕玄联合力荐!大型互联网公司开发应用实践!) 许令波 著 ISBN 978-7-121- ...
随机推荐
- pat 1027 Colors in Mars(20 分)
1027 Colors in Mars(20 分) People in Mars represent the colors in their computers in a similar way as ...
- hdu 1083 Courses (最大匹配)
CoursesTime Limit: 20000/10000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Su ...
- 用maven创建web项目(spring Mvc)
用maven创建web项目(spring Mvc) 1.打开cmd进入到你要创建maven项目的目录下: 2.输入以下命令.然后根据提示输入相应的groupId.artifactId.version: ...
- GentOS 7 安装步骤
附上原作者的博客网址: https://blog.csdn.net/qq_42570879/article/details/82853708 1.CentOS下载CentOS是免费版,推荐在官网上直接 ...
- 理解Redis单线程运行模式
本文首发于:https://mp.weixin.qq.com/s/je4nqCIq6ARhSV2V5Ymmtg 微信公众号:后端技术指南针 0.概述 通过本文将了解到以下内容: Redis服务器采用单 ...
- element 时间限制 结束时间大于开始时间 数组形式
组件中 绑定focus时间 <el-form-item v-for="(item, index) in ruleForm.yunqiDateArr" :key="i ...
- mysql通俗易懂的数据库连接池原理及模拟实现
什么是数据库连接池? 当系统使用JDBC技术访问数据库时会创建一个connection对象,而该对象的创建过程是非常消耗资源的,并且创建对象的时间也特别长,假设系统一天有1万次的访问量,那么一天就会有 ...
- Redis 的底层数据结构(对象)
目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...
- 【NHOI2018】跳伞登山赛
[题目描述] 某山区有高高低低的 n 个山峰,根据海拔高度的不同,这些山峰由低到高进行了 1 到 n 编号.有 m 条只能单向通行的羊肠小道连接这些山峰.现在,这里要举行一场跳伞登山赛,选手们伞降到某 ...
- 【Luogu P1090】合并果子
Luogu P1090 [解题思路] 刚看到这题的时候,第一反应就是每次取两个最小,然后重新排序,再取最小.但是这样会TLE. 既然找最小的,那就可以利用单调队列了.显然输入的数据是不具有单调性的,但 ...