《编写可维护的 JavaScript》编程实践 - 第 10 章 抛出自定义错误

编程实践 - 第 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 {
    // 处理其他的错误
  }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值