异常后清理

异常的效果是另一种控制流。 每个可能导致异常的操作(几乎每个函数调用和属性访问)都可能导致控制流突然离开你的代码。

这意味着当代码有几个副作用时,即使它的“常规”控制流看起来像它们总是会发生,但异常可能会阻止其中一些发生。

这是一些非常糟糕的银行代码。

  1. const accounts = {
  2. a: 100,
  3. b: 0,
  4. c: 20
  5. };
  6. function getAccount() {
  7. let accountName = prompt("Enter an account name");
  8. if (!accounts.hasOwnProperty(accountName)) {
  9. throw new Error(`No such account: ${accountName}`);
  10. }
  11. return accountName;
  12. }
  13. function transfer(from, amount) {
  14. if (accounts[from] < amount) return;
  15. accounts[from] -= amount;
  16. accounts[getAccount()] += amount;
  17. }

transfer函数将一笔钱从一个给定的账户转移到另一个账户,在此过程中询问另一个账户的名称。 如果给定一个无效的帐户名称,getAccount将引发异常。

但是transfer首先从帐户中删除资金,之后调用getAccount,之后将其添加到另一个帐户。 如果它在那个时候由异常中断,它就会让钱消失。

这段代码本来可以更智能一些,例如在开始转移资金之前调用getAccount。 但这样的问题往往以更微妙的方式出现。 即使是那些看起来不像是会抛出异常的函数,在特殊情况下,或者当他们包含程序员的错误时,也可能会这样。

解决这个问题的一个方法是使用更少的副作用。 同样,计算新值而不是改变现有数据的编程风格有所帮助。 如果一段代码在创建新值时停止运行,没有人会看到这个完成一半的值,并且没有问题。

但这并不总是实际的。 所以try语句具有另一个特性。 他们可能会跟着一个finally块,而不是catch块,也不是在它后面。 finally块会说“不管发生什么事,在尝试运行try块中的代码后,一定会运行这个代码。”

  1. function transfer(from, amount) {
  2. if (accounts[from] < amount) return;
  3. let progress = 0;
  4. try {
  5. accounts[from] -= amount;
  6. progress = 1;
  7. accounts[getAccount()] += amount;
  8. progress = 2;
  9. } finally {
  10. if (progress == 1) {
  11. accounts[from] += amount;
  12. }
  13. }
  14. }

这个版本的函数跟踪其进度,如果它在离开时注意到,它中止在创建不一致的程序状态的位置,则修复它造成的损害。

请注意,即使finally代码在异常退出try块时运行,它也不会影响异常。finally块运行后,堆栈继续展开。

即使异常出现在意外的地方,编写可靠运行的程序也非常困难。 很多人根本就不关心,而且由于异常通常针对异常情况而保留,因此问题可能很少发生,甚至从未被发现。 这是一件好事还是一件糟糕的事情,取决于软件执行失败时会造成多大的损害。