## 八、Bug 和错误

> 原文：[Bugs and Errors](http://eloquentjavascript.net/08_error.html)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
> 
> 部分参考了[《JavaScript 编程精解（第 2 版）》](https://book.douban.com/subject/26707144/)

> 调试的难度是开始编写代码的两倍。 因此，如果你尽可能巧妙地编写代码，那么根据定义，你的智慧不足以进行调试。
> 
> Brian Kernighan 和 P.J. Plauger，《The Elements of Programming Style》

![](img/8-0.jpg)

计算机程序中的缺陷通常称为 bug。 它让程序员觉得很好，将它们想象成小事，只是碰巧进入我们的作品。 实际上，当然，我们自己把它们放在了那里。

如果一个程序是思想的结晶，你可以粗略地将错误分为因为思想混乱引起的错误，以及思想转换为代码时引入的错误。 前者通常比后者更难诊断和修复。

## 语言

计算机能够自动地向我们指出许多错误，如果它足够了解我们正在尝试做什么。 但是这里 JavaScript 的宽松是一个障碍。 它的绑定和属性概念很模糊，在实际运行程序之前很少会发现拼写错误。 即使这样，它也允许你做一些不会报错的无意义的事情，比如计算`true *'monkey'`。

JavaScript 有一些报错的事情。 编写不符合语言语法的程序会立即使计算机报错。 其他的东西，比如调用不是函数的东西，或者在未定义的值上查找属性，会导致在程序尝试执行操作时报告错误。

不过，JavaScript 在处理无意义的计算时，会仅仅返回`NaN`（表示不是数字）或`undefined`这样的结果。程序会认为其执行的代码毫无问题并顺利运行下去，要等到随后的运行过程中才会出现问题，而此时已经有许多函数使用了这个无意义的值。程序执行中也可能不会遇到任何错误，只会产生错误的程序输出。找出这类错误的源头是非常困难的。

我们将查找程序中的错误或者 bug 的过程称为调试（debug）。

## 严格模式

当启用了严格模式（strict mode）后，JavaScript 就会在执行代码时变得更为严格。我们只需在文件或函数体顶部放置字符串`"use strict"`就可以启用严格模式了。下面是示例代码：

```js
function canYouSpotTheProblem() {
  "use strict";
  for (counter = 0; counter < 10; counter++) {
    console.log("Happy happy");
  }
}

canYouSpotTheProblem();
// → ReferenceError: counter is not defined
```

通常，当你忘记在绑定前面放置`let`时，就像在示例中的`counter`一样，JavaScript 静静地创建一个全局绑定并使用它。 在严格模式下，它会报告错误。 这非常有帮助。 但是，应该指出的是，当绑定已经作为全局绑定存在时，这是行不通的。 在这种情况下，循环仍然会悄悄地覆盖绑定的值。

严格模式中的另一个变化是，在未被作为方法而调用的函数中，`this`绑定持有值`undefined`。 当在严格模式之外进行这样的调用时，`this`引用全局作用域对象，该对象的属性是全局绑定。 因此，如果你在严格模式下不小心错误地调用方法或构造器，JavaScript 会在尝试从`this`读取某些内容时产生错误，而不是愉快地写入全局作用域。

例如，考虑下面的代码，该代码不带`new`关键字调用构造器，以便其`this`不会引用新构造的对象：

```js
function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // oops
console.log(name);
// → Ferdinand
```

虽然我们错误调用了`Person`，代码也可以执行成功，但会返回一个未定义值，并创建名为`name`的全局绑定。而在严格模式中，结果就不同了。

```js
"use strict";
function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand");
// → TypeError: Cannot set property 'name' of undefined
```

JavaScript 会立即告知我们代码中包含错误。这种特性十分有用。

幸运的是，使用`class`符号创建的构造器，如果在不使用`new`来调用，则始终会报错，即使在非严格模式下也不会产生问题。

严格模式做了更多的事情。 它不允许使用同一名称给函数赋多个参数，并且完全删除某些有问题的语言特性（例如`with`语句，这是错误的，本书不会进一步讨论）。

简而言之，在程序顶部放置`"use strict"`很少会有问题，并且可能会帮助你发现问题。

## 类型

有些语言甚至在运行程序之前想要知道，所有绑定和表达式的类型。 当类型以不一致的方式使用时，他们会马上告诉你。 JavaScript 只在实际运行程序时考虑类型，即使经常尝试将值隐式转换为它预期的类型，所以它没有多大帮助。

尽管如此，类型为讨论程序提供了一个有用的框架。 许多错误来自于值的类型的困惑，它们进入或来自一个函数。 如果你把这些信息写下来，你不太可能会感到困惑。

你可以在上一章的`goalOrientedRobot`函数上面，添加一个像这样的注释来描述它的类型。

```js
// (WorldState, Array) → {direction: string, memory: Array}
function goalOrientedRobot(state, memory) {
  // ...
}
```

有许多不同的约定，用于标注 JavaScript 程序的类型。

关于类型的一点是，他们需要引入自己的复杂性，以便能够描述足够有用的代码。 你认为从数组中返回一个随机元素的`randomPick`函数的类型是什么？ 你需要引入一个绑定类型`T`，它可以代表任何类型，这样你就可以给予`randomPick`一个像`([T])->T`的类型（从`T`到`T`的数组的函数）。

当程序的类型已知时，计算机可以为你检查它们，在程序运行之前指出错误。 有几种 JavaScript 语言为语言添加类型并检查它们。 最流行的称为 [TypeScript](https://www.typescriptlang.org/)。 如果你有兴趣为你的程序添加更多的严谨性，我建议你尝试一下。

在本书中，我们将继续使用原始的，危险的，非类型化的 JavaScript 代码。

## 测试

如果语言不会帮助我们发现错误，我们将不得不努力找到它们：通过运行程序并查看它是否正确执行。

一次又一次地手动操作，是一个非常糟糕的主意。 这不仅令人讨厌，而且也往往是无效的，因为每次改变时都需要花费太多时间来详尽地测试所有内容。

计算机擅长重复性任务，测试是理想的重复性任务。 自动化测试是编写测试另一个程序的程序的过程。 编写测试比手工测试有更多的工作，但是一旦你完成了它，你就会获得一种超能力：它只需要几秒钟就可以验证，你的程序在你编写为其测试的所有情况下都能正常运行。 当你破坏某些东西时，你会立即注意到，而不是在稍后的时间里随机地碰到它。

测试通常采用小标签程序的形式来验证代码的某些方面。 例如，一组（标准的，可能已经由其他人测试过）`toUpperCase`方法的测试可能如下：

```js
function test(label, body) {
  if (!body()) console.log(`Failed: ${label}`);
}

test("convert Latin text to uppercase", () => {
  return "hello".toUpperCase() == "HELLO";
});
test("convert Greek text to uppercase", () => {
  return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ";
});
test("don't convert case-less characters", () => {
  return "مرحبا".toUpperCase() == "مرحبا";
});
```

像这样写测试往往会产生很多重复，笨拙的代码。 幸运的是，有些软件通过提供适合于表达测试的语言（以函数和方法的形式），并在测试失败时输出丰富的信息来帮助你构建和运行测试集合（测试套件，test suite）。 这些通常被称为测试运行器（test runner）。

一些代码比其他代码更容易测试。 通常，代码与外部交互的对象越多，建立用于测试它的上下文就越困难。 上一章中显示的编程风格，使用自包含的持久值而不是更改对象，通常很容易测试。

## 调试

当程序的运行结果不符合预期或在运行过程中产生错误时，你就会注意到程序出现问题了，下一步就是要推断问题出在什么地方。

有时错误很明显。错误消息会指出错误出现在程序的哪一行，只要稍加阅读错误描述及出错的那行代码，你一般就知道如何修正错误了。

但不总是这样。 有时触发问题的行，只是第一个地方，它以无效方式使用其他地方产生的奇怪的值。 如果你在前几章中已经解决了练习，你可能已经遇到过这种情况。

下面的示例代码尝试将一个整数转换成给定进制表示的字符串（十进制、二进制等），其原理是：不断循环取出最后一位数字，并将其除以基数（将最后一位数从数字中除去）。但该程序目前的输出表明程序中是存在bug的。

```js
function numberToString(n, base = 10) {
  let result = "", sign = "";
  if (n < 0) {
    sign = "-";
    n = -n;
  }
  do {
    result = String(n % base) + result;
    n /= base;
  } while (n > 0);
  return sign + result;
}
console.log(numberToString(13, 10));
// → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…
```

你可能已经发现程序运行结果不对了，不过先暂时装作不知道。我们知道程序运行出了问题，试图找出其原因。

这是一个地方，你必须抵制随机更改代码来查看它是否变得更好的冲动。 相反，要思考。 分析正在发生的事情，并提出为什么可能发生的理论。 然后，再做一些观察来检验这个理论 - 或者，如果你还没有理论，可以进一步观察来帮助你想出一个理论。

有目的地在程序中使用`console.log`来查看程序当前的运行状态，是一种不错的获取额外信息的方法。在本例中，我们希望`n`的值依次变为 13，1，然后是 0。让我们先在循环起始处输出`n`的值。

```
13
1.3
0.13
0.013
…
1.5e-323
```

没错。13 除以 10 并不会产生整数。我们不应该使用`n/=base`，而应该使用`n=Math.floor(n/base)`，使数字“右移”，这才是我们实际想要的结果。

使用`console.log`来查看程序行为的替代方法，是使用浏览器的调试器（debugger）功能。 浏览器可以在代码的特定行上设置断点（breakpoint）。 当程序执行到带有断点的行时，它会暂停，并且你可以检查该点的绑定值。 我不会详细讨论，因为调试器在不同浏览器上有所不同，但请查看浏览器的开发人员工具或在 Web 上搜索来获取更多信息。

设置断点的另一种方法，是在程序中包含一个`debugger`语句（仅由该关键字组成）。 如果你的浏览器的开发人员工具是激活的，则只要程序达到这个语句，程序就会暂停。

## 错误传播

不幸的是，程序员不可能避免所有问题。 如果你的程序以任何方式与外部世界进行通信，则可能会导致输入格式错误，工作负荷过重或网络故障。

如果你只为自己编程，那么你就可以忽略这些问题直到它们发生。 但是如果你创建了一些将被其他人使用的东西，你通常希望程序比只是崩溃做得更好。 有时候，正确的做法是不择手段地继续运行。 在其他情况下，最好向用户报告出了什么问题然后放弃。 但无论在哪种情况下，该程序都必须积极采取措施来回应问题。

假设你有一个函数`promptInteger`，要求用户输入一个整数并返回它。 如果用户输入`"orange"`，它应该返回什么？

一种办法是返回一个特殊值，通常会使用`null`，`undefined`或 -1。

```js
function promptNumber(question) {
  let result = Number(prompt(question, ""));
  if (Number.isNaN(result)) return null;
  else return result;
}

console.log(promptNumber("How many trees do you see?"));
```

现在，调用`promptNumber`的任何代码都必须检查是否实际读取了数字，否则必须以某种方式恢复 - 也许再次询问或填充默认值。 或者它可能会再次向它的调用者返回一个特殊值，表示它未能完成所要求的操作。

在很多情况下，当错误很常见并且调用者应该明确地考虑它们时，返回特殊值是表示错误的好方法。 但它确实有其不利之处。 首先，如果函数已经可能返回每一种可能的值呢？ 在这样的函数中，你必须做一些事情，比如将结果包装在一个对象中，以便能够区分成功与失败。

```js
function lastElement(array) {
  if (array.length == 0) {
    return {failed: true};
  } else {
    return {element: array[array.length - 1]};
  }
}
```

返回特殊值的第二个问题是它可能产生非常笨拙的代码。 如果一段代码调用`promptNumber` 10 次，则必须检查是否返回`null` 10 次。 如果它对`null`的回应是简单地返回`null`本身，函数的调用者将不得不去检查它，以此类推。

## 异常

当函数无法正常工作时，我们只希望停止当前任务，并立即跳转到负责处理问题的位置。这就是异常处理的功能。

异常是一种当代码执行中遇到问题时，可以触发（或抛出）异常的机制，异常只是一个普通的值。触发异常类似于从函数中强制返回：异常不只跳出到当前函数中，还会跳出函数调用方，直到当前执行流初次调用函数的位置。这种方式被称为“堆栈展开（Unwinding the Stack）”。你可能还记得我们在第3章中介绍的函数调用栈，异常会减小堆栈的尺寸，并丢弃所有在缩减程序栈尺寸过程中遇到的函数调用上下文。

如果异常总是会将堆栈尺寸缩减到栈底，那么异常也就毫无用处了。它只不过是换了一种方式来彻底破坏你的程序罢了。异常真正强大的地方在于你可以在堆栈上设置一个“障碍物”，当异常缩减堆栈到达这个位置时会被捕获。一旦发现异常，你可以使用它来解决问题，然后继续运行该程序。

```js
function promptDirection(question) {
  let result = prompt(question, "");
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new Error("Invalid direction: " + result);
}

function look() {
  if (promptDirection("Which way?") == "L") {
    return "a house";
  } else {
    return "two angry bears";
  }
}

try {
  console.log("You see", look());
} catch (error) {
  console.log("Something went wrong: " + error);
}
```

`throw`关键字用于引发异常。 异常的捕获通过将一段代码包装在一个`try`块中，后跟关键字`catch`来完成。 当`try`块中的代码引发异常时，将求值`catch`块，并将括号中的名称绑定到异常值。 在`catch`块结束之后，或者`try`块结束并且没有问题时，程序在整个`try / catch`语句的下面继续执行。

在本例中，我们使用`Error`构造器来创建异常值。这是一个标准的 JavaScript 构造器，用于创建一个对象，包含`message`属性。在多数 JavaScript 环境中，构造器实例也会收集异常创建时的调用栈信息，即堆栈跟踪信息（Stack Trace）。该信息存储在`stack`属性中，对于调用问题有很大的帮助，我们可以从堆栈跟踪信息中得知问题发生的精确位置，即问题具体出现在哪个函数中，以及执行失败为止调用的其他函数链。

需要注意的是现在`look`函数可以完全忽略`promptDirection`出错的可能性。这就是使用异常的优势：只有在错误触发且必须处理的位置才需要错误处理代码。其间的函数可以忽略异常处理。

嗯，我们要讲解的理论知识差不多就这些了。

## 异常后清理

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

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

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

```js
const accounts = {
  a: 100,
  b: 0,
  c: 20
};

function getAccount() {
  let accountName = prompt("Enter an account name");
  if (!accounts.hasOwnProperty(accountName)) {
    throw new Error(`No such account: ${accountName}`);
  }
  return accountName;
}

function transfer(from, amount) {
  if (accounts[from] < amount) return;
  accounts[from] -= amount;
  accounts[getAccount()] += amount;
}
```

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

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

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

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

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

```js
function transfer(from, amount) {
  if (accounts[from] < amount) return;
  let progress = 0;
  try {
    accounts[from] -= amount;
    progress = 1;
    accounts[getAccount()] += amount;
    progress = 2;
  } finally {
    if (progress == 1) {
      accounts[from] += amount;
    }
  }
}
```

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

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

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

## 选择性捕获

当程序出现异常且异常未被捕获时，异常就会直接回退到栈顶，并由 JavaScript 环境来处理。其处理方式会根据环境的不同而不同。在浏览器中，错误描述通常会写入 JavaScript 控制台中（可以使用浏览器工具或开发者菜单来访问控制台）。我们将在第 20 章中讨论的，无浏览器的 JavaScript 环境 Node.js 对数据损坏更加谨慎。 当发生未处理的异常时，它会中止整个过程。

对于程序员的错误，让错误通行通常是最好的。 未处理的异常是表示糟糕的程序的合理方式，而在现代浏览器上，JavaScript 控制台为你提供了一些信息，有关在发生问题时堆栈上调用了哪些函数的。

对于在日常使用中发生的预期问题，因未处理的异常而崩溃是一种糟糕的策略。

语言的非法使用方式，比如引用一个不存在的绑定，在null中查询属性，或调用的对象不是函数最终都会引发异常。你可以像自己的异常一样捕获这些异常。

进入`catch`语句块时，我们只知道`try`体中引发了异常，但不知道引发了哪一类或哪一个异常。

JavaScript（很明显的疏漏）并未对选择性捕获异常提供良好的支持，要不捕获所有异常，要不什么都不捕获。这让你很容易假设，你得到的异常就是你在写`catch`时所考虑的异常。

但它也可能不是。 可能会违反其他假设，或者你可能引入了导致异常的 bug。 这是一个例子，它尝试持续调用`promptDirection`，直到它得到一个有效的答案：

```js
for (;;) {
  try {
    let dir = promtDirection("Where?"); // ← typo!
    console.log("You chose ", dir);
    break;
  } catch (e) {
    console.log("Not a valid direction. Try again.");
  }
}
```

我们可以使用`for (;;)`循环体来创建一个无限循环，其自身永远不会停止运行。我们在用户给出有效的方向之后会跳出循环。但我们拼写错了`promptDirection`，因此会引发一个“未定义值”错误。由于`catch`块完全忽略了异常值，假定其知道问题所在，错将绑定错误信息当成错误输入。这样不仅会引发无限循环，而且会掩盖掉真正的错误消息——绑定名拼写错误。

一般而言，只有将抛出的异常重定位到其他地方进行处理时，我们才会捕获所有异常。比如说通过网络传输通知其他系统当前应用程序的崩溃信息。即便如此，我们也要注意编写的代码是否会将错误信息掩盖起来。

因此，我们转而会去捕获那些特殊类型的异常。我们可以在`catch`代码块中判断捕获到的异常是否就是我们期望处理的异常，如果不是则将其重新抛出。那么我们该如何辨别抛出异常的类型呢？

我们可以将它的`message`属性与我们所期望的错误信息进行比较。 但是，这是一种不稳定的编写代码的方式 - 我们将使用供人类使用的信息来做出程序化决策。 只要有人更改（或翻译）该消息，代码就会停止工作。

我们不如定义一个新的错误类型，并使用`instanceof`来识别异常。

```js
class InputError extends Error {}

function promptDirection(question) {
  let result = prompt(question);
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new InputError("Invalid direction: " + result);
}
```

新的错误类扩展了`Error`。 它没有定义它自己的构造器，这意味着它继承了`Error`构造器，它需要一个字符串消息作为参数。 事实上，它根本没有定义任何东西 - 这个类是空的。 `InputError`对象的行为与`Error`对象相似，只是它们的类不同，我们可以通过类来识别它们。

现在循环可以更仔细地捕捉它们。

```js
for (;;) {
  try {
    let dir = promptDirection("Where?");
    console.log("You chose ", dir);
    break;
  } catch (e) {
    if (e instanceof InputError) {
      console.log("Not a valid direction. Try again.");
    } else {
      throw e;
    }
  }
}
```

这里的`catch`代码只会捕获`InputError`类型的异常，而其他类型的异常则不会在这里进行处理。如果又输入了不正确的值，那么系统会向用户准确报告错误——“绑定未定义”。

## 断言

断言（assertions）是程序内部的检查，用于验证某个东西是它应该是的方式。 它们并不是用于处理正常操作中可能出现的情况，而是发现程序员的错误。

例如，如果`firstElement`被描述为一个函数，永远不会在空数组上调用，我们可以这样写：

```js
function firstElement(array) {
  if (array.length == 0) {
    throw new Error("firstElement called with []");
  }
  return array[0];
}
```

现在，它不会默默地返回未定义值（当你读取一个不存在的数组属性的时候），而是在你滥用它时立即干掉你的程序。 这使得这种错误不太可能被忽视，并且当它们发生时更容易找到它们的原因。

我不建议尝试为每种可能的不良输入编写断言。 这将是很多工作，并会产生非常杂乱的代码。 你会希望为很容易犯（或者你发现自己做过）的错误保留他们。

## 本章小结

错误和无效的输入十分常见。编程的一个重要部分是发现，诊断和修复错误。 如果你拥有自动化测试套件或向程序添加断言，则问题会变得更容易被注意。

我们常常需要使用优雅的方式来处理程序可控范围外的问题。如果问题可以就地解决，那么返回一个特殊的值来跟踪错误就是一个不错的解决方案。或者，异常也可能是可行的。

抛出异常会引发堆栈展开，直到遇到下一个封闭的`try/catch`块，或堆栈底部为止。`catch`块捕获异常后，会将异常值赋予`catch`块，`catch`块中应该验证异常是否是实际希望处理的异常，然后进行处理。为了有助于解决由于异常引起的不可预测的执行流，可以使用`finally`块来确保执行`try`块之后的代码。

## 习题

### 重试

假设有一个函数`primitiveMultiply`，在 20% 的情况下将两个数相乘，在另外 80% 的情况下会触发`MultiplicatorUnitFailure`类型的异常。编写一个函数，调用这个容易出错的函数，不断尝试直到调用成功并返回结果为止。

确保只处理你期望的异常。

```js
class MultiplicatorUnitFailure extends Error {}

function primitiveMultiply(a, b) {
  if (Math.random() < 0.2) {
    return a * b;
  } else {
    throw new MultiplicatorUnitFailure();
  }
}

function reliableMultiply(a, b) {
  // Your code here.
}

console.log(reliableMultiply(8, 8));
// → 64
```

### 上锁的箱子

考虑以下这个编写好的对象：

```js
const box = {
  locked: true,
  unlock() { this.locked = false; },
  lock() { this.locked = true;  },
  _content: [],
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this._content;
  }
};
```

这是一个带锁的箱子。其中有一个数组，但只有在箱子被解锁时，才可以访问数组。不允许直接访问`_content`属性。

编写一个名为`withBoxUnlocked`的函数，接受一个函数类型的参数，其作用是解锁箱子，执行该函数，无论是正常返回还是抛出异常，在`withBoxUnlocked`函数返回前都必须锁上箱子。

```js
const box = {
  locked: true,
  unlock() { this.locked = false; },
  lock() { this.locked = true;  },
  _content: [],
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this._content;
  }
};

function withBoxUnlocked(body) {
  // Your code here.
}

withBoxUnlocked(function() {
  box.content.push("gold piece");
});

try {
  withBoxUnlocked(function() {
    throw new Error("Pirates on the horizon! Abort!");
  });
} catch (e) {
  console.log("Error raised:", e);
}
console.log(box.locked);
// → true
```
