编程实践 - 第 10 章 抛出自定义错误
《编写可维护的 JavaScript》—— Nicholas C. Zakas
在 JavaScript 中抛出错误是一门艺术,明白代码中哪些地方合适抛出错误是需要时间的。
一旦搞明白这一点,调试代码的时间将大大缩段。
1. 错误的本质
当某些非期望的事情发生时,程序就引发一个错误,
也许是给函数传递了不正确的值,或者数学运算碰到了一个无效的操作数。
编程语言定义了一组基本的规则,当偏离了这些规则时将导致错误,然后开发者能修复错误。
如果错误没有被抛出或者报告给你,调试将会非常困难;
如果所有失败都是悄无声息的,那将消耗你大量的时间发现它,更不要说单独隔离并修复它。
所以错误是开发者的朋友,而不是敌人。
错误常常在非期望的地方、不恰当的时机跳出来,这很麻烦。
更糟糕的是,默认的错误消息通常太简洁而无法解释到底什么东西出错了。
JavaScript 错误消息以消息稀少、隐晦含糊而被人所不喜,特别是在老版本 IE 中,这会让问题更加复杂化。
如果跳出的错误能像这样描述:“由于这个原因,该函数调用失败”。
那么,调试将会非常简单,这正是抛出自己的错误的好处。
像失败案例一样来考虑错误是非常有帮助的。在代码的某个特殊的地方处理失败,比在所有地方预期失败要简单地多。
在产品设计上,这是非常普遍的实践经验。
汽车有碰撞吸收区域,这些区域框架的设计旨在撞击发生时,以可预测的方式毁坏。
知道一个碰撞发生时,碰撞框架将如何反应(哪些部分将失败),制造商将能保证乘客的安全,你的代码也可以用这种方法来创建。
2. 在 JavaScript 中抛出错误
使用 throw
操作符抛出一个 Error
对象。
throw new Error("Something bad happened.");
内置的 Error
类型在所有的 JavaScript 实现中都是有效的,它只接受一个参数,即错误消息(message)。
当抛出的 Error
类的对象没有被捕获时,浏览器会在控制台显示构造器中的 message
字符串。
如果抛出的值一个不是 Error
类的对象,不是所有浏览器都会按你预期显示错误消息,
// 不好的写法
throw "messaage";
throw { name: "Nicholas" };
throw true;
throw 123456;
throw new Date();
3. 抛出错误的好处
抛出自己的错误可以明确告知别人错误的原因,有助于调试。如总是在错误消息中包含函数名称。
function getById(id) {
if (typeof id === "string" && id.length > 0) {
return document.getElementById(id);
} else {
throw new Error("getById(): Argument must be a string.");
}
}
4. 何时抛出错误
理解如何抛出错误是一方面,另一方面要理解在什么时候抛出错误。
由于 JavaScript 没有类型检查和参数检查,大量的开发者错误地认为应该实现每个函数的类型检查。
这种做法并不实际,并且会对脚本的整体性能造成影响。如下面的函数,它试图实现充分的类型检查。
// 不好的写法:检查了太多的错误
function addClass(element, className) {
if (!element || typeof element.className !== "string") {
throw new Error("addClass(): First argument must be a DOM element.");
}
if (typeof className !== "string") {
throw new Error("addClass(): Second argument must be a string.");
}
element.className += " " + className;
}
辨识代码中哪些部分在特定的情况下最有可能导致失败,并只在那些地方抛出错误才是关键所在。
在上例中,最有可能引发错误的是给函数第一个参数传递一个 null 引用;
如果第二个参数是 null 或其他什么,只会导致 DOM 元素的显示不符合期望,但不至于提高到严重错误的程度。
所以,只检查 DOM 元素就够了。
// 好的写法
function addClass(element, className) {
if (!element || typeof element.className != "string") {
throw new Error("addClass(): First argument must be a DOM element.");
}
element.className += " " + className;
}
如果一个函数只被已知的实体调用,错误检测很可能没有必要,比如私有函数;
如果不能提前确定函数会被调用的所有地方,则需要一些错误检查。
抛出错误最佳的地方是在工具函数中,如 JavaScript 类库。
针对已知条件引发的错误,所有的 JavaScript 类库都应该从它们的公共接口里抛出错误。
如 jQuery、YUI、Dojo 等大型的库,不可能预测到你在什么情况下调用它们的 API,
但是当你做错事时它们应该通知你,因为你不可能进入库代码中去调试错误的原因。
函数的调用栈应该在进入库代码 API 时就终止,不应该深入代码实现的细节。
类库提供了对功能实现的抽象,目的是让开发者更便捷,而抛出错误有助于隐藏这些实现细节。
抛出错误的经验法则:
- 一旦修复了一个很难调试的错误,尝试增加一两个自定义错误。当再次发生错误时,这将有助于更容易地解决问题。
- 如果正在编写代码,思考一下: “我希望 “某些事情” 不会发生,如果发生,我的代码会一团糟”。这是,如果 “某些事情” 发生,就抛出一个错误。
- 如果别人也会用到你正在编写的代码,思考一下他们使用的方式,在特定的情况下抛出错误。
请牢记:我们的目的不是防止错误,而是在错误发生时更加容易地调试。
5. try-catch
语句
JavaScript 提供了 try-catch
语句,用于捕获并处理错误。
可能引发错误的代码放在 try
块中,处理错误的代码放在 catch
块中,如:
try {
doSomethingThatMightCauseAnError();
} catch(ex) {
handleError(ex);
}
当 try
块中发生了一个错误,程序立即停止,然后跳转到 catch
块中,并传入一个错误对象,该错误对象有助于从错误中恢复。
当然,还可以增加一个 finally
块,不管错误是否发生,都会执行。
如果 try
块中包包 return
语句,则必须等到 finally
块执行完毕才会返回。
使用 throw
还是 try-catch
?
开发者很难敏锐判断是抛出一个错误还是捕获错误。
错误只应该从应用程序栈中最深的地方抛出,如类库中的代码;
任何处理应用程序特定逻辑的代码都应该有错误处理的能力,并且捕获从底层组件中抛出的错误。
应用程序逻辑总是知道为何调用某个特定的函数,因此也是最适合处理错误的。
千万不要将 catch
块留空,应该处理错误,如果知道可能要发生的错误,那肯定知道如何从错误中恢复。
6. 错误类型
ECMA-262 规范指出了 7 种错误类型。这些类型在 JavaScript 引擎中都有用到,当然也可以手动创建。
Error
:所有错误的基本类型EvalError
:通过eval()
函数执行代码发生错误时抛出RangeError
:一个数字超出边界时抛出。非常罕见,如new Array(-20)
创建 -20 长度的数组ReferenceError
:期望的对象不存在。如在null
对象上调用方法,null.sayHello()
SyntaxError
:给eval()
函数传递的代码中有语法错误时抛出。TypeError
:变量不是期望的类型时抛出,如new 10
、"prop" in true
。URIError
:给encodeURI() encodeURIComponent() decodeURI() decodeURIComponent()
传递格式非法的 URI 字符串时抛出
通过检查特定的错误类型可以更可靠地处理错误
try {
// 可能引发错误的代码
} catch(ex) {
if (ex instanceof TypeError) {
// ...
} else if (ex instanceof ReferenceError) {
// ...
} else {
// 其他错误处理
}
}
浏览器抛出 Error
对象会附加一些额外的信息,如行号、列号、堆栈、源码信息等。
如果直接抛出 Error
对象就不能区分自定义的错误和浏览器的错误了。
解决方案是创建的自定义错误继承自 Error
类。
function MyError(message) {
this.message = message;
}
MyError.prototype = new Error();
接下来就可以抛出一个 MyError 的实例对象,使浏览器能想处理原生错误一样做出响应。
try {
throw new MyError("Hello world!");
} catch (ex) {
if (ex instanceof MyError) {
// 处理自己的错误
} else {
// 处理其他的错误
}
}