Глава 8
Обработка на Bugs и Error

Debugging е два пъти по-силно, отколкото самото писане на код. Ето защо ако напишете код, колкото и хитър да е по дефиниция, той не е достатъчно умен за да може сам да си отстранява грешките.”

Brian Kernighan and P.J. Plauger, The Elements of Programming Style

Ян-ма написал малка програма, в която използвал много глобални променливи и калпави комбинации. Четейки я, един студент попитал: „Ти ни предупреди за тези техники, но аз ги намирам във вашата програма. Как е възможно това?” А майсторът казал:”Не е нужно да се носи вода, когато къщата не гори.””

Master Yuan-Ma, Книгата на програмиране

Програмата кристализира мисълта. Понякога тези мисли са объркани. Друг път се правят грешки при превръщането на мисълта в код. Така или иначе, резултата е грешна програма.

Пропуски в програмата обикновено се наричат грешки ("bugs"). Bugs може да бъде грешка на програмиста или проблеми в други системи, с които програмата си взаимодейства. Някои грешки са очевидни, а други едва доловими и може да останат скрити в една система в продължение на години.

Често проблемите на повърхността са само, когато една програма се сблъска със ситуация, за която програмиста първоначално не е помислил. Понякога такива ситуации са неизбежни. Например, когато потребителя иска да въведе възраст и пол - портокал, това поставя програмата в трудна позиция. Ситуацията трябва да се очаква и обработва по някакъв начин.

Грешки на програмиста

Когато става дума за грешки на програмиста, нашата цел е ясна. Ние искаме да ги намерим и поправим. Такива грешки могат да варират от прости правописни грешки, които са причина компютъра да се оплаче веднага, до невидими за очите фини грешки, при които програмата работи, създавайки неправилни резултати само в определени случаи. При грешки от втория вид може да отнеме седмици, за да се диагностицират.

Степента, с която езиците ви помагат да намерите подобни грешки варира. Не е изненадващо, че JavaScript - “помага за всичко” е в края на тази скала. Някои езици искат да знаят типа на всички променливи и изрази преди изпълнение на програмата и ще ви кажат веднага, ако даден тип се използва по грешен начин. JavaScript отчита типовете при действителното изпълнение на програмата, а дори и тогава позволява да се правят някои очевидно безсмислени неща без да се оплаква, като x = true * "monkey".

Има някои неща, от които JavaScript се оплаква, все пак. Писане на програма, която не е синтактично валидна, веднага ще предизвика грешка. Друго нещо е, като извикате нещо, което не е функция или разглежда свойство на неопределена стойност, ще предизвика грешка без да се докладва, но когато се изпълнява програмата се натъкваме на безсмислени действия.

Често, безсмислените изчисления, ще произведат просто NaN (не число) или неопределена стойност. И програмата щастливо продължава, убедена че прави нещо смислено. Грешката се появява едва по-късно, след като фалшивата стойност е минала през няколко функции. Тя не може да предизвика грешка във всички, но безшумно определя изхода на програмата да бъде грешен. Намирането на източника на тези проблеми може да бъде много трудно.

Процесът за намиране на грешки - бъгове в програмите се нарича debugging.

Strict mode

JavaScript може да се направи малко по-стриктен, като се използва strict mode. Това се прави чрез поставяне на string "use strict" в горната част на файла или тялото на функцията. Ето един пример:

edit & run code by clicking it
function canYouSpotTheProblem() { "use strict"; for (counter = 0; counter < 10; counter++) console.log("Happy happy"); } canYouSpotTheProblem(); // → ReferenceError: counter is not defined

Обикновено, когато сте пропуснали да поставите var пред вашата променлива, както примера с counter, JavaScirpt тихо създава глобална променлива и я използва. В строг режим обаче, грешката се съобщава. Това е много полезно. Трябва да се отбележи обаче, че това не работи, когато въпросната променлива вече съществува, като глобална променлива, а само при определянето и.

Друга промяна в строг режим е, че this обвързването съдържа стойност undefined във функции, които не са извикани, като методи. При извършване на такова извикване извън строг режим, this се отнася до глобалния обхват на обекта. Така че, ако случайно се извика метод или конструктор неправилно в строг режим, JavaScript ще съобщи за грешка, веднага след като се опита да прочете нещо от this, а не радостно да работи с глобалния обект, да създава и чете глобални променливи.

Например, нека разгледаме следния код, който призовава конструктор без ключовата дума new, така че this няма да се отнася до ново-изградения обект.

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

Така фалшивото извикване към Person успя, но се връща недефинирана стойност и се създава глобалната променлива name. В строг режим, резултата ще е по-различен

"use strict";
function Person(name) { this.name = name; }
// Oops, forgot 'new'
var ferdinand = Person("Ferdinand");
// → TypeError: Cannot set property 'name' of undefined

Тука веднага каза, че нещо не е наред. Това е от полза.

Строг режим прави още няколко неща. Той забранява подаването на множество параметри със същото име към функция и премахва някои проблемни изикови функции изцяло (подобно на with което толкова е заблудено, че не се разглежда в тази книга).

С една дума, поставянето на "use strict" в горната част на вашата програма рядко боли и може да ви помогне да забележите някакъв проблем.

Тестване

Ако езикът не може да направи много, за да ви помогне в откриването на грешки ще трябва да ги търсите по трудния начин: чрез стартиране на програмата и гледане дали тя прави правилното нещо.

Правейки това на ръка, отново и отново, е сигурен начин да ви докара до лудост. За щастие, често е възможно да се напише програма, която автоматизира повторното тестване на програмата.

Като пример, отново ще използваме типа Vector.

function Vector(x, y) {
  this.x = x;
  this.y = y;
}
Vector.prototype.plus = function(other) {
  return new Vector(this.x + other.x, this.y + other.y);
};

Ще напишем програма, която да провери нашето изпълнение на Vector, дали работи по предназначение. След това, всеки път, когато се промени изпълнението, ще използваме програмата за тестване, така че да бъдем сигурни, че не сме счупили нещо. Когато добавяме допълнителна функционалност (например нов метод) към типа Vector, ние също така ще добавим и тестове за новата функция.

function testVector() {
  var p1 = new Vector(10, 20);
  var p2 = new Vector(-10, 5);
  var p3 = p1.plus(p2);

  if (p1.x !== 10) return "fail: x property";
  if (p1.y !== 20) return "fail: y property";
  if (p2.x !== -10) return "fail: negative x property";
  if (p3.x !== 0) return "fail: x from plus";
  if (p3.y !== 25) return "fail: y from plus";
  return "everything ok";
}
console.log(testVector());
// → everything ok

Писането на тестове, като този, имат тенденцията да правят по-скоро неудобен повтарящ се код. За щастие съществува софтуер, който да ни помогне да изградим и пуснем колекции от тестове (тестов комплект), чрез предоставяне на език (под формата на функции и методи) подходящ за изразяване на тестове и чрез извеждане на информация, когато теста се провали. Те се наричат testing frameworks (тестови рамки).

Debugging

След като забележите, че нещо не е наред с вашата програма, защото тя не е с нормално поведение или произвежда грешки, следващата стъпка е да разберете какъв е проблемът.

Понякога е очевидно. Съобщаването на грешка посочва конкретен ред от вашата програма и ако се вгледате в описанието на този ред от кода, често може да видите проблема.

Но не винаги. Понякога реда, който е задействал проблема е просто първото място, където фалшивата стойност произведена другаде ще проработи по неправилен начин. Понякога не е съобщение за грешка, а просто невалиден резултат. Ако сте решавали упражненията в предните глави, вероятно вече сте имали такива ситуации.

В следният пример, програмата се опитва да превърне цяло число в string на някаква база (десетична, двоична и т.н.), като на няколко пъти взема последната цифра и след това я разделя на броя за да се отърве от тази цифра. Но на изхода произвежда някакъв ненормален резултат и се предполага, че това е грешка.

function numberToString(n, base) {
  var 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. Нека да напишем стойността в началото на цикъла.

13
1.3
0.13
0.013
…
1.5e-323

Правилно. Разделянето на 13 от 10 не произвежда цяло число. Вместо n /= base, всъщност искаме n = Math.floor(n / base), така че числото да бъде правилно променено от дясно.

Една алтернатива на използването на console.log е да се използва debugger на вашия браузър. Съвременните браузъри разполагат с възможност за определяне на точка на прекъсване на определен ред от вашия код. Това ще доведе до пауза при изпълнението на вашата програма, всеки път, когато се достигне реда с точката за прекъсване "breakpoint" и ще ви позволи да инспектирате стойностите на променливите до този момент. Аз няма да влизам в подробности тука, тъй като дебъгерите се различават в различните браузъри, но ги разгледайте в инструментите за разработчици на браузъра си и потърсете в Интернет повече информация. Друг начин да се определи точка на прекъсване е да включите debugger изявление във вашата програма (състоящо се просто, от ключовата дума). Ако инструментите за разработчици в браузъра са активни програмата ще направи пауза, когато стигне това изявление и ще бъдете в състояние да инспектирате съдържанието.

Разпространяване на Error

Не всички проблеми могат да бъдат предотвратени от програмиста, за съжаление. Ако вашата програма комуникира с външния свят по някакъв начин, има шанс данните на входа, които получава да са невалидни или други системи с които се опитва да се свърже са счупени или недостъпни.

Прости програми или програми, които работят само под вашето ръководство, могат да си позволят просто да се откажат, когато възникне такъв проблем. Ще разгледате проблема и ще опитате отново. Реалните приложения, от друга страна, не се очаква просто да се сринат. Понякога правилното нещо, което може да направите е да вземете грешното въвеждане и да продължите работа. В други случаи е по-добре да се докладва на потребителя, какво се е объркало и след това да се откажете. Но и в двете ситуации, програмата може да направи нещо активно в отговор на проблема.

Да речем, че функцията promptInteger пита потребителя за цяло число и го връща. Какво трябва да върне, ако потребителя въведе на входа “портокал”?

Единият вариант е да върне специална стойност. Избора за тази стойност е null или undefined.

function promptNumber(question) {
  var result = Number(prompt(question, ""));
  if (isNaN(result)) return null;
  else return result;
}

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

Това е ефективна стратегия. Сега всеки код, който извиква promptNumber трябва да провери дали действително чете цифра и ако не, трябва по някакъв начин да се възстанови, може би като попита отново или чрез попълване на стойност по подразбиране. Или може да върне специална стойност към повикващия за да покаже, че той не е направил това, което е бил помолен.

В много ситуации, най-вече, когато грешките са често срещани и на повикващия трябва изрично да се каже да ги вземе под внимание, връщайки специална стойност е перфектен начин да се покаже грешката. Той обаче има своите недостатъци. Първо, какво ще стане ако функцията е готова да върне всяка възможна стойност? За такава функция е трудно да се намери специална стойност, която може да бъде разграничена от валиден резултат.

Вторият проблем с връщането на специални стойности е, че те могат да доведат до някой много затрупан код. Ако част от кода извиква promptNumber 10 пъти, трябва да се провери 10 пъти дали null e върнатата стойност. И ако отговора за намирането на null е просто да върне себе си на повикващия, той от своя страна трябва да провери за него и т.н.

Изключения (Exceptions)

Когато дадена функция не може да продължи нормално, това което бихме искали да направи е просто да спре и веднага да скочи обратно към място, за което знае как да се справи с проблема. Това се нарича обработка на изключения.

Изключенията са механизъм, който позволява на кода при проблем да хвърли грешка, което е просто една стойност. Хвърлянето на грешка донякъде прилича на супер - връщане от функция: то изкача не само от текущата функция, но и от нейните извикващи, чак до първото извикване, с което започва текущото изпълнение. Това се нарича развиване на стека. Може би си спомняте за стека за извикване на функции в Глава 3. Изключенията влизат надолу в този стек, изхвърляйки всички контексти на операциите, с които се сблъсква.

Ако изключенията винаги стигат чак до дъното на стека, те не биха били от голяма полза. Те могат да предоставят нов начин да взривите вашата програма. Тяхната сила се крие във факта, че можете да зададете “препятствия” по протежение на стека, за да направят изключение, ако влезе по на долу. След това може да направите нещо с него, след което програмата продължава да работи до мястото, където е уловила това изключение.

Ето един пример:

function promptDirection(question) {
  var 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 ще бъде обвързана със стойността на изключението. След като catch блока приключи или ако try блока премине без проблем, контрола продължава след try/catch изявлението.

В такъв случай ние използваме Error конструктора за създаване на нашата стойност за изключение. Той е стандартен JavaScript конструктор, който създава обект със свойство message. В съвременните условия на JavaScript, инстанции на този конструктор също събират информация за стека на извикване, когато изключението се е създало, така наречената стек следа. Тази информация се съхранява в stack свойство и може да бъде полезна, когато се опитваме да определим един проблем: тя ни казва точната функция, където е възникнал проблема и какави други функции са довели до създаването на провала.

Имайте в предвид, че функцията look напълно пренебрегва възможността, че promptDirection може да се обърка. Това е най-голямото предимство на кода за обработка на изключения, за който е необходима само точката, където се появява грешката и точката, където се обработва. Функциите между тях могат да забравят за него.

Е, почти.....

Почистване след изключения

Нека разгледаме следната ситуация: функцията withContext иска да се увери, че по време на нейното изпълнение, променливата на най-високо ниво context притежава специфична context стойност. След това завършва и възстановява на тази променлива старата и стойност.

var context = null;

function withContext(newContext, body) {
  var oldContext = context;
  context = newContext;
  var result = body();
  context = oldContext;
  return result;
}

Какво става ако body предизвика изключение? В този случай извикването на withContext ще бъде изхвърлено от стека заедно с изключението и context никога няма да се върне към старата си стойност.

Има още една особеност, която try изявленията имат. Те могат да бъдат последвани от finally блок, вместо или в допълнение към catch блока. Finally блока казва ”Няма значение какво се случва, изпълни този код, след като мине през try блока”. Ако една функция трябва да почисти нещо, кода за почистване обикновено се поставя в finally блока.

function withContext(newContext, body) {
  var oldContext = context;
  context = newContext;
  try {
    return body();
  } finally {
    context = oldContext;
  }
}

Имайте в предвид, че вече не се налага да съхраняваме резултата на body (който искаме да се върне) в променлива. Дори да се върне директно от try блока, finally блока пак ще се изпълни. Сега можем да направим това и да бъде безопасно.

try {
  withContext(5, function() {
    if (context < 10)
      throw new Error("Not enough context!");
  });
} catch (e) {
  console.log("Ignoring: " + e);
}
// → Ignoring: Error: Not enough context!

console.log(context);
// → null

Въпреки, че функцията извикана от withContext гръмна, самата withContext правилно ще почисти context променливата.

Подбиращо прихващане

Когато едно изключение премине през целия път до дъното на стека, без да е хванато, то се обработва от заобикалящата среда. Какво означава това, се различава между средите. В браузъри описанието за грешка обикновено се получава написано в конзолата на JavaScript (достъпна чрез Tools на браузъра с F12 или меню Developer).

За програмист грешки или проблеми, с които програмата не може да се справи, просто се разрешават да преминат и често е наред. Едно необработено изключение е един разумен начин да се сигнализира за счупена програма и конзолата на JavaScript в съвременните браузъри ви предоставя някаква информация за извикванията на функцията в стека, когато е възникнал проблема.

За проблеми, които се очаква да се случат по време на рутинна употреба, сблъскване с необработено изключение не е много приятелски отговор.

Невалидна употреба на езика, като се позовава на несъществуваща променлива, разглеждане на свойство null или се извика нещо, което не е функция, също ще доведе до изключение. Такива изключения могат да бъдат прихванати, точно като вашите собствени изключения.

Когато catch тялото е въведено, всичко което знаем е, че нещо в try тялото ще причини изключение. Но ние незнаем, какво или кое изключение го е причинило.

JavaScript (има един доста фрапантен пропуск) не предоставя пряка подкрепа за избирателно прихващане на изключения: да го хване или да не го хване. Това прави много лесно предположението, че изключението, което получавате е това, за което се мисли, когато е написано в catch блока.

Но може и да не бъде. Някое друго предположение може да бъде нарушено или може да сте въвели грешка някъде и това да е причина за изключение. Ето един пример, който се опитва да запази извикването към promptDirection, докато стане валиден отговор:

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

Конструкцията for (;;) е начин за умишлено създаване на цикъл, който не спира да работи. Можем да се измъкнем от цикъла, когато дадем валидна посока. Но и името на променливата написано, като promptDirection е грешно, което ще доведе до “undefined variable” (неидентифицирана променлива). Тъй като catch блока напълно игнорира стойноста на своето изключение ((e)) и ако приемем, че знае какъв е проблема, той неправилно третира грешката, като посочва грешно въвеждане. Не само, че причинява един безкраен цикъл, но и също така “затваря” полезното съобщение за грешка за неправилно изписана променлива.

Общо правило е, да не правим blanket-catch за изключения, освен ако нямаме намерение да ги пращаме някъде, например, по мрежата за да кажем на друга система, че програмата ни е счупена. Дори и тогава помислете внимателно, как може да скриете информация.

Така че, ние искаме да прихванем специфичен вид изключение. Можем да направим това чрез проверка в catch блока дали изключението, което получаваме е това, което ни интересува и го прехвърляме в друго. Но как да разпознаем това изключение?

Разбира се, бихме могли да съчетаем неговото свойство message със съобщението за грешката, което очакваме. Но това е по-нестабилен начин за писане на код - така се лишаваме от използването на информацията предназначена за усвояване от човека (съобщението), за да направим програмно решение. Веднага, след като се промени (или се преведе) съобщението, кодът ще спре да работи.

По- скоро, нека да дефинираме нов тип грешка и да използваме instanceof, за да го идентифицира.

function InputError(message) {
  this.message = message;
  this.stack = (new Error()).stack;
}
InputError.prototype = Object.create(Error.prototype);
InputError.prototype.name = "InputError";

Прототипът е направен да извлича от Error.prototype, така че instanceof Error също ще върне true за InputError обекти. Той също дава свойството name, тъй като стандартните типове грешки, като Error, SyntaxError, ReferenceError и т.н.) също имат това свойство.

Присвояването към stack свойството се опитва да даде на този обект малко по-полезна следа в стека, за платформи, които го поддържат, чрез създаване на обект за нормална грешка и след това използвате този обект на stack, като свой собствен.

Сега promptDirection може да хвърли такава грешка.

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

И цикълът може да я прихване по-прецизно:

for (;;) {
  try {
    var 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;
  }
}

Това ще хване само случаи на InputError и несвързани изключения. Ако се въведе отново печатна грешка или неопределена променлива, грешката ще бъде надлежно съобщена.

Твърдения

Твърденията са инструмент за основни разсъждения за проверка за грешки на програмиста. Помислете за помощната функция assert:

function AssertionFailed(message) {
  this.message = message;
}
AssertionFailed.prototype = Object.create(Error.prototype);

function assert(test, message) {
  if (!test)
    throw new AssertionFailed(message);
}

function lastElement(array) {
  assert(array.length > 0, "empty array in lastElement");
  return array[array.length - 1];
}

Това осигурява компактен начин да се наложат очаквания - услужливо гръмване на програмата, ако посоченото условие не изпълнено. Например, функцията lastElement, която извлича последния елемент от масив, ще върне undefined за празени масиви, ако твърдението е пропуснато. Извличането на последния елемент от празен масив няма много смисъл, така че почти е сигурно, че грешката е на програмиста.

Твърденията са начин да се уверите, че грешките причиняват повреди на мястото на грешката, а не мълчаливо произвеждащи глупости, които могат да продължат да предизвикват проблеми в нямаща нищо общо част на системата.

Резюме

Грешки и лош вход са факти от живота. Грешките в програмите трябва да бъдат намерени и фиксирани. Те могат станат по-лесно забележими с наличието на автоматизирани тестове и добавяне на твърдения във вашите програми.

Проблеми причинени от фактори, които са извън контрола на програмата обикновено трябва да се обработват грациозно. По някога, когато проблемът е на местно ниво, специалните стойности за връщане са един нормален начин да ги проследят. В противен случай, изключенията са за предпочитане.

Хвърляне на изключение създава призив към стека да се развие до следващия обхващащ try/catch блок или до дъното на стека. Стойността на изключението, ще бъде дадено на catch блока, който го прихваща и се уверява, че това всъщност е очаквания вид изключение, а след това прави нещо с него. За да се справят с непредвидим контрол причинен от изключения, блоковете на finally могат да бъдат използвани за да се гарантира, че част от кода винаги ще се стартира.

Упражнения

Опитайте отново

Да речем, че функцията primitiveMultiply в 50% от случаите, умножава две числа, а в останалите 50%, извиква изключение от типа MultiplicatorUnitFailure. Напишете функция, която завършва тази неудобна функция и продължава да опитва докато операцията стане успешна, след което връща резултата.

Уверете се, че сте разгледали само изключенията, които се опитват да се справят.

function MultiplicatorUnitFailure() {}

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

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

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

Извикването на primitiveMultiply очевидно трябва да се случи в try блок. Съответният catch блок трябва да rethrow изключение, когато не е инстанция на MultiplicatorUnitFailure и повикването да се повтори, когато това е гарантирано.

За да направите повторен опит, може да използвате един цикъл, който breaks само, когато повикването е успяло - както в примера по-рано в тази глава - или използвате рекурсия и надявам се да не получите поредица от неуспехи, толкова дълго, че да препълните стека (което е доста безопасен залог).

Заключената кутия

Да разгледаме следния (по скоро измислен) обект:

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

Това е кутия с ключалка. Вътре има масив, който може да получите само, когато кутията е отключена. Директен достъп до свойството _content не е позволен.

Напишете функция, наречена withBoxUnlocked, която взема стойността на функцията, като аргумент, отключва кутията, стартира функцията и след това гарантира, че кутията отново е заключена преди да се върне, независимо от това дали функцията аргумент се е върнала нормално или хвърля изключение.

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

За допълнителни точки, се уверете, че ако извикате withBoxUnlocked, когато кутията е вече отключена тя остава отключена.

Това упражнение изисква finally блок, както може би сте предположили. Вашата функция трябва първо да отключи кутията и след това да извика функцията с аргумент за вътрешноста на try тялото. Накрая finally блока трябва да заключи кутията отново.

За да сме сигурни, че не заключваме кутията, когато тя вече е била заключена, проверете заключването в началото на функцията и отключете и заключете, само когато тя е започнала заключена.