第3版が利用可能です。こちらで読んでください

第8章
バグとエラー処理

デバッグは、コードを書くことの2倍の難易度です。したがって、可能な限り巧妙にコードを書くと、定義上、デバッグできるほど賢くはありません。

Brian Kernighan and P.J. Plauger, プログラミング作法

Yuan-Maは、多くのグローバル変数と粗末なショートカットを使った小さなプログラムを書いていました。それを読んで、ある生徒が尋ねました。「あなたはこれらのテクニックに反対するよう警告しましたが、あなたのプログラムにはそれらが見られます。これはどうしたことでしょうか?」 師は言いました。「家が火事になっていないのに、ホースを取りに行く必要はありません。」

Master Yuan-Ma, プログラミングの書

プログラムは結晶化された思考です。時には、それらの思考は混乱しています。また、思考をコードに変換する際にミスが入り込むこともあります。いずれにしても、その結果、欠陥のあるプログラムが生まれます。

プログラムの欠陥は、通常バグと呼ばれます。バグは、プログラマーのエラー、またはプログラムが対話する他のシステムの問題である可能性があります。すぐに明らかになるバグもあれば、微妙でシステムに何年も隠れたままになるバグもあります。

多くの場合、問題は、プログラマーが当初考慮していなかった状況にプログラムが遭遇した場合にのみ表面化します。そのような状況は避けられない場合があります。ユーザーに年齢を入力するように求められ、オレンジと入力された場合、これはプログラムを困難な立場に置きます。状況を予測し、何らかの方法で処理する必要があります。

プログラマーのミス

プログラマーのミスに関しては、私たちの目的はシンプルです。それらを見つけて修正したいのです。そのようなミスは、プログラムが目にした瞬間にコンピュータが文句を言うような単純なタイプミスから、プログラムの動作方法の理解における微妙なミスまであり、特定の状況でのみ誤った結果が生じます。後者のタイプのバグは、診断に数週間かかる場合があります。

そのようなミスを見つけるのに言語がどの程度役立つかは様々です。当然のことながら、JavaScriptはそのスケールの中で「ほとんど役に立たない」側に位置しています。一部の言語は、プログラムを実行する前にすべての変数と式の型を知りたがり、型が矛盾した方法で使用されている場合はすぐに教えてくれます。JavaScriptは、実際にプログラムを実行しているときにのみ型を考慮し、それでも、x = true * "monkey"のように、明らかに無意味なことを文句なしに行うことができます。

しかし、JavaScriptが文句を言うこともあります。構文的に有効でないプログラムを作成すると、すぐにエラーが発生します。関数ではないものを呼び出したり、未定義の値のプロパティを調べたりするなど、他のことは、プログラムが実行されて無意味なアクションに遭遇したときにエラーが報告される原因となります。

しかし、多くの場合、無意味な計算は単にNaN(数値ではない)または未定義の値を生成します。そして、プログラムはそれが何か意味のあることをしていると確信して、幸せに続きます。このミスは、偽の値がいくつかの関数を通過した後、後で初めて明らかになります。エラーがまったく発生しないかもしれませんが、プログラムの出力が間違っている原因となる可能性があります。このような問題の原因を見つけるのは難しい場合があります。

プログラムのバグ(ミス)を見つけるプロセスは、デバッグと呼ばれます。

厳格モード

JavaScriptは、厳格モードを有効にすることで、少し厳格にすることができます。これは、ファイルまたは関数本体の先頭に文字列"use strict"を配置することによって行われます。次に例を示します

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

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

通常、例のようにcounterの前にvarを付けるのを忘れると、JavaScriptは静かにグローバル変数を作成して使用します。ただし、厳格モードでは、代わりにエラーが報告されます。これは非常に役立ちます。ただし、問題の変数がすでにグローバル変数として存在する場合、これには機能せず、それに割り当てると作成される場合にのみ機能することに注意してください。メソッドとして呼び出されない関数では、thisバインディングは値undefinedを保持します。厳格モードの外部でこのような呼び出しを行うと、thisはグローバルスコープオブジェクトを参照します。そのため、厳格モードで誤ってメソッドまたはコンストラクターを呼び出すと、JavaScriptはグローバルオブジェクトを操作してグローバル変数を作成および読み取るのではなく、thisから何かを読み取ろうとするとすぐにエラーを生成します。

厳格モードのもう1つの変更点は、メソッドとして呼び出されない関数では、`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"` を配置しても害になることはめったになく、問題の発見に役立つ可能性があります。

テスト

言語がミスの発見にあまり役立たない場合は、難しい方法で見つける必要があります。プログラムを実行して、正しい動作をしているかどうかを確認します。

これを何度も手動で行うと、確実に気が狂ってしまいます。幸いなことに、実際のプログラムのテストを自動化する2番目のプログラムを作成できることがよくあります。

例として、もう一度 `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

このようなテストを作成すると、かなり反復的でぎこちないコードが生成される傾向があります。幸いなことに、テストを表現するのに適した言語(関数とメソッドの形式で)を提供し、テストが失敗したときに有益な情報を出力することによって、テストの集合(*テストスイート*)の構築と実行を支援するソフトウェアが存在します。これらは *テストフレームワーク* と呼ばれます。

デバッグ

プログラムが誤動作したりエラーを生成したりするため、プログラムに問題があることに気付いたら、次の手順は問題が *何であるか* を特定することです。

明らかな場合もあります。エラーメッセージはプログラムの特定の行を指しており、エラーの説明とそのコード行を見ると、多くの場合、問題がわかります。

しかし、常にそうとは限りません。問題を引き起こした行は、単に他の場所で生成された偽の値が無効な方法で使用される最初の場所である場合があります。また、エラーメッセージがまったくない場合もあります。単に無効な結果が生じるだけです。前の章の演習を解いてきた場合、おそらくすでにそのような状況を経験しているでしょう。

次のサンプルプログラムは、最後の桁を繰り返し選んでから数値を割ってこの桁を取り除くことにより、整数を任意の基数(10進数、2進数など)の文字列に変換しようとします。しかし、現在生成されている正気でない出力は、バグがあることを示唆しています。

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` ステートメント(単にそのキーワードで構成されます)を含めることです。ブラウザの開発者ツールがアクティブな場合、プログラムはそのステートメントに到達するたびに一時停止し、その状態を調べることができます。

エラーの伝播

残念ながら、すべての問題をプログラマーが防ぐことができるわけではありません。プログラムが何らかの方法で外部の世界と通信する場合、取得する入力が無効であるか、通信しようとする他のシステムが壊れているか到達不能である可能性があります。

単純なプログラム、または監督下でのみ実行されるプログラムは、そのような問題が発生した場合に単にあきらめることができます。問題を調べて、再試行します。一方、「実際の」アプリケーションは、単にクラッシュしないことが期待されています。正しい対応は、不正な入力をそのまま受け入れて実行を続けることである場合があります。そうでない場合は、ユーザーに何が問題だったかを報告してからあきらめる方がよいでしょう。ただし、どちらの場合でも、プログラムは問題に aktiv 対応して何かをする必要があります。

ユーザーに整数を要求して返す関数 `promptInteger` があるとします。ユーザーが *オレンジ* と入力した場合、何を返す必要がありますか?

1つの選択肢は、特別な値を返すようにすることです。そのような値の一般的な選択肢は、nullundefined です。

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 を呼び出すコードはすべて、実際の数値が読み取られたかどうかを確認し、そうでない場合は、何らかの方法で回復する必要があります。たとえば、再度問い合わせるか、デフォルト値を入力します。あるいは、呼び出し元に特別な値を返して、要求された処理に失敗したことを示すこともできます。

多くの状況、主にエラーが一般的であり、呼び出し側が明示的にエラーを考慮に入れる必要がある場合、特別な値を返すことはエラーを示すための完全に適切な方法です。しかし、それには欠点があります。まず、関数がすでにあらゆる種類の値を返すことができる場合はどうでしょうか?そのような関数の場合、有効な結果と区別できる特別な値を見つけるのは困難です。

特別な値を返すことの2つ目の問題は、コードが非常に煩雑になる可能性があることです。コードの一部が promptNumber を10回呼び出す場合、null が返されたかどうかを10回確認する必要があります。そして、null を見つけた場合の応答が単に null を返すだけの場合、呼び出し側はさらにそれをチェックする必要があり、それが続きます。

例外

関数が正常に処理を続行できない場合、私たちがしたいことは、実行中の処理を停止し、問題の処理方法を知っている場所にすぐにジャンプすることです。これが *例外処理* の役割です。

例外は、問題が発生したコードが例外(単なる値)を *発生*(または *スロー*)させることを可能にするメカニズムです。例外の発生は、関数からのスーパーチャージされたreturnにいくぶん似ています。現在の関数だけでなく、呼び出し元からも、現在の実行を開始した最初の呼び出しまで、すべてジャンプアウトします。これは *スタックの巻き戻し* と呼ばれます。第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 コンストラクターを使用して例外値を作成しました。これは、message プロパティを持つオブジェクトを作成する標準のJavaScriptコンストラクターです。最新のJavaScript環境では、このコンストラクターのインスタンスは、例外が作成されたときに存在していた呼び出しスタックに関する情報、いわゆる *スタックトレース* も収集します。この情報は stack プロパティに格納され、問題のデバッグに役立ちます。問題が発生した正確な関数と、失敗した呼び出しにつながった他の関数を示します。

関数 look は、promptDirection が失敗する可能性を完全に無視していることに注意してください。これが例外の大きな利点です。エラー処理コードは、エラーが発生した時点とエラーが処理された時点でのみ必要です。間の関数は、それについてすべて忘れることができます。

まあ、ほとんど...

例外後のクリーンアップ

次の状況を考えてみましょう。関数 withContext は、実行中にトップレベル変数 context が特定のコンテキスト値を保持していることを確認したいと考えています。終了後、この変数を古い値に復元します。

var context = null;

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

body が例外を発生させたらどうでしょうか?その場合、withContext の呼び出しは例外によってスタックからスローされ、context は古い値に設定されません。

try ステートメントにはもう1つ機能があります。catch ブロックの代わりに、または catch ブロックに加えて、finally ブロックを続けることができます。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コンソール(ブラウザのツールまたは開発者メニューからアクセス可能)に書き込まれます。

プログラマーのミスやプログラムがどうしても処理できない問題の場合、エラーをそのままにすることが適切な場合がよくあります。処理されない例外は、壊れたプログラムを通知する合理的な方法であり、最新のブラウザでは、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 のスペルを間違えました。これは「未定義の変数」エラーになります。catch ブロックは例外値(e)を完全に無視し、問題が何であるかを知っていると想定しているため、変数エラーを誤って不正な入力として扱います。これは無限ループを引き起こすだけでなく、スペルミスのある変数に関する有用なエラーメッセージを「埋め込み」ます。

一般的なルールとして、例外をどこかに「ルーティング」するため(たとえば、ネットワーク経由で別のシステムにプログラムがクラッシュしたことを伝えるため)でない限り、例外を包括的にキャッチしないでください。そして、その時でも、情報をどのように隠しているかについて注意深く考えてください。

したがって、*特定の*種類の例外をキャッチしたいと考えています。これは、取得した例外が目的の例外であるかどうかを 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 から派生するように作られているため、InputError オブジェクトに対しても instanceof Error は true を返します。標準エラータイプ(ErrorSyntaxErrorReferenceError など)にもそのようなプロパティがあるため、name プロパティも指定されています。

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ブロックに渡されます。catchブロックは、それが実際に予期された種類の例外であることを確認し、それに対して何らかの処理を行う必要があります。例外によって引き起こされる予測不可能な制御フローに対処するために、finallyブロックを使用して、ブロックが終了したときにコードの一部が*常に*実行されるようにすることができます。

演習

再試行

50%の確率で2つの数を乗算し、残りの50%でMultiplicatorUnitFailureタイプの例外を発生させる関数primitiveMultiplyがあるとします。この不格好な関数をラップし、呼び出しが成功するまで試行を続け、その後結果を返す関数を記述してください。

処理しようとしている例外のみを処理するようにしてください。

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ブロックは、MultiplicatorUnitFailureのインスタンスでない場合は例外を再スローし、インスタンスの場合は呼び出しが再試行されるようにする必要があります。

再試行を行うには、この章の前半のlookの例のように、呼び出しが成功した場合にのみ中断するループを使用するか、再帰を使用し、スタックオーバーフローを引き起こすほど長い失敗の連鎖が発生しないことを期待します(これはかなり安全な賭けです)。

ロックされたボックス

次の(やや不自然な)オブジェクトを考えてみましょう。

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ブロックは、ボックスを再びロックする必要があります。

既にロックされていないボックスをロックしないようにするには、関数の開始時にロックを確認し、最初にロックされていた場合にのみロックを解除してロックします。