バグとエラー

デバッグは、そもそもコードを書くことの2倍難しい。したがって、可能な限り巧妙にコードを書くと、定義上、それをデバッグするのに十分な賢さがないということになる。

ブライアン・カーニハンとP.J.プラウガー, プログラミング作法
Illustration showing various insects and a centipede

コンピュータプログラムの欠陥は、通常バグと呼ばれます。プログラマーは、それらがたまたま私たちの作業に忍び込んだ小さなものだと想像して気分を良くします。実際には、もちろん、私たちはそれらを自分たちでそこに置いているのです。

プログラムが結晶化した思考であるならば、バグは、思考が混乱していることが原因のものと、思考をコードに変換する際に導入された間違いが原因のものに大まかに分類できます。前者のタイプは、一般的に後者よりも診断と修正が困難です。

言語

私たちが何をしようとしているかをコンピュータが十分に知っていれば、多くの間違いは自動的に指摘される可能性があります。しかし、ここにおいて、JavaScriptの緩さは妨げになります。そのバインディングとプロパティの概念は曖昧であるため、実際にプログラムを実行するまでタイプミスを検出することはほとんどありません。それでも、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

クラスとモジュール(第10章で説明します)内のコードは、自動的に厳格になります。古い非厳格な動作は、古いコードの一部がそれに依存している可能性があるという理由だけで依然として存在し、言語設計者は既存のプログラムを破壊しないように努力しています。

通常、例のcounterのように、バインディングの前にletを付けるのを忘れると、JavaScriptはグローバルバインディングを静かに作成して使用します。厳格モードでは、代わりにエラーが報告されます。これは非常に役立ちます。ただし、問題のバインディングがすでにスコープ内のどこかに存在する場合は、これは機能しないことに注意する必要があります。その場合、ループは依然として静かにバインディングの値を上書きします。

厳格モードでのもう1つの変更点は、thisバインディングが、メソッドとして呼び出されない関数で値undefinedを保持することです。厳格モードの外でそのような呼び出しを行うと、thisはグローバルスコープオブジェクトを参照します。これは、そのプロパティがグローバルバインディングであるオブジェクトです。したがって、厳格モードで誤ってメソッドまたはコンストラクタを誤って呼び出すと、JavaScriptはthisから何かを読み取ろうとするとすぐにエラーを生成するのではなく、グローバルスコープに正常に書き込みます。

たとえば、newキーワードなしでコンストラクタ関数を呼び出す次のコードについて考えてみましょう。これにより、そのthisは新しく構築されたオブジェクトを参照しません

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

Personへの偽の呼び出しは成功しましたが、未定義の値を返し、グローバルバインディングnameを作成しました。厳格モードでは、結果は異なります。

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

何かが間違っていることがすぐにわかります。これは役立ちます。

幸いなことに、class表記で作成されたコンストラクタは、newなしで呼び出されると常に文句を言うため、非厳格モードでも問題が少なくなります。

厳格モードでは、さらにいくつかのことが行われます。関数に同じ名前の複数のパラメーターを付けることを禁止し、特定の問題のある言語機能を完全に削除します(たとえば、withステートメントなど、この本ではこれ以上議論しないほど間違っています)。

要するに、プログラムの先頭に"use strict"を配置すると、ほとんどの場合害はなく、問題を発見するのに役立つ可能性があります。

一部の言語では、プログラムを実行する前に、すべてのバインディングと式の型を知りたいと考えています。型が一貫性のない方法で使用されている場合は、すぐに教えてくれます。JavaScriptは、プログラムを実際に実行するときにのみ型を考慮し、そこでも多くの場合、値を期待される型に暗黙的に変換しようとするため、あまり役に立ちません。

それでも、型はプログラムについて話すための便利なフレームワークを提供します。多くの間違いは、関数に出入りする値の種類について混乱していることが原因です。その情報が書き留められていると、混乱する可能性が低くなります。

前の章のfindRoute関数の前に、次のようなコメントを追加して、その型を記述できます。

// (graph: Object, from: string, to: string) => string[]
function findRoute(graph, from, to) {
  // ...
}

JavaScriptプログラムに型を注釈するためのさまざまな規則があります。

型に関する1つのことは、有用であるために十分なコードを記述できるように、独自の複雑さを導入する必要があるということです。配列からランダムな要素を返すrandomPick関数の型は何だと思いますか?任意の型を表すことができる型変数Tを導入する必要があります。これにより、randomPick(T[]) → Tのような型(Tの配列からTへの関数)を与えることができます。

プログラムの型がわかっている場合、コンピュータがプログラムをチェックして、プログラムを実行する前に間違いを指摘することができます。言語に型を追加してチェックするJavaScriptの方言がいくつかあります。最も人気のあるものはTypeScriptと呼ばれています。プログラムにさらに厳密さを加えたい場合は、ぜひ試してみてください。

この本では、引き続き未加工の危険な型付けされていないJavaScriptコードを使用します。

テスト

言語が間違いを見つけるのにあまり役立たない場合、私たちは大変な方法でそれらを見つける必要があります。プログラムを実行して、それが正しいことをしているかどうかを確認することで見つける必要があります。

これを何度も手動で行うのは本当に悪い考えです。面倒なだけでなく、変更を加えるたびにすべてを徹底的にテストするのに時間がかかりすぎるため、非効率になる傾向があります。

コンピュータは反復的なタスクが得意であり、テストは理想的な反復的なタスクです。自動テストとは、別のプログラムをテストするプログラムを作成するプロセスです。テストを作成することは、手動でテストするよりも少し手間がかかりますが、いったん完了すると、一種のスーパーパワーを得ることができます。テストを作成したすべての状況でプログラムが依然として適切に動作することを確認するのに数秒しかかかりません。何かを壊した場合は、後でランダムに遭遇するのではなく、すぐに気づくでしょう。

テストは通常、コードのいくつかの側面を検証する小さなラベル付きプログラムの形式をとります。たとえば、(標準であり、おそらく他の誰かがすでにテストした)toUpperCaseメソッドのテストのセットは次のようになります。

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() == "مرحبا";
});

このようなテストの書き方は、かなり反復的でぎこちないコードになりがちです。幸いなことに、テストを表現するのに適した言語(関数やメソッドの形)を提供し、テストが失敗したときには有益な情報を出力することで、テストの集合(テストスイート)を構築および実行するのに役立つソフトウェアが存在します。これらは通常、テストランナーと呼ばれます。

コードの中には、他のコードよりもテストしやすいものがあります。一般的に、コードが対話する外部オブジェクトが多いほど、それをテストするコンテキストを設定するのが難しくなります。 前の章で示した、変更するオブジェクトではなく、自己完結型の永続的な値を使用するプログラミングスタイルは、テストしやすい傾向があります。

デバッグ

プログラムの動作がおかしい、またはエラーが発生したために、プログラムに何か問題があることに気づいたら、次のステップは、問題が何かを特定することです。

時々、それは明白です。エラーメッセージはプログラムの特定の行を指し示し、エラーの説明とそのコード行を見ると、しばしば問題がわかります。

しかし、常にそうとは限りません。問題を引き起こした行は、単に他の場所で生成された不安定な値が不正な方法で使用された最初の場所にすぎない場合があります。前の章の演習を解いてきた場合は、おそらくすでにそのような状況を経験したことがあるでしょう。

次の例のプログラムは、最後の桁を繰り返し取り出して数を割ることで、整数を特定の基数(10進数、2進数など)の文字列に変換しようとしています。しかし、現在出力される奇妙な出力は、バグがあることを示唆しています。

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 には、131、そして 0 の値をとってほしいと思っています。ループの開始時にその値を書き出してみましょう。

13
1.3
0.13
0.013
…
1.5e-323

そうですね。13 を 10 で割っても整数にはなりません。 n /= base の代わりに、実際には n = Math.floor(n / base) とする必要があります。これにより、数が正しく右に「シフト」されます。

console.logを使用してプログラムの動作を覗き見る代わりに、ブラウザーのデバッガー機能を使用することもできます。ブラウザーには、コードの特定の行にブレークポイントを設定する機能が備わっています。プログラムの実行がブレークポイントのある行に到達すると、プログラムは一時停止し、その時点でのバインディングの値を調べることができます。デバッガーはブラウザーによって異なるため、詳細は説明しませんが、ブラウザーの開発者ツールを確認するか、Web で手順を検索してください。

ブレークポイントを設定する別の方法は、プログラムにdebuggerステートメント(単にそのキーワードで構成される)を含めることです。ブラウザーの開発者ツールがアクティブな場合、プログラムはそのようなステートメントに到達するたびに一時停止します。

エラーの伝播

残念ながら、プログラマーがすべての問題を防ぐことができるわけではありません。プログラムが何らかの方法で外部世界と通信する場合、不正な形式の入力が得られたり、作業で過負荷になったり、ネットワークが失敗したりする可能性があります。

自分自身のためにのみプログラミングしている場合は、問題が発生するまで無視してもかまいません。しかし、他の人が使用するものを構築する場合は、通常、プログラムがクラッシュするだけよりも優れた動作をさせたいと思うでしょう。場合によっては、悪い入力をそのまま受け入れて実行を続けるのが正しい場合もあります。他の場合は、何がうまくいかなかったのかをユーザーに報告して、あきらめる方が良い場合もあります。いずれの場合も、プログラムは問題に対応して何かを積極的に行う必要があります。

ユーザーに数値を尋ねてそれを返す関数promptNumberがあるとします。ユーザーが「オレンジ」と入力した場合、何を返す必要がありますか?

1つのオプションは、特別な値を返すことです。このような値の一般的な選択肢は、nullundefined、または-1です。

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

多くの場合、特にエラーが一般的で、呼び出し元が明示的にエラーを考慮する必要がある場合は、特別な値を返すことはエラーを示す良い方法です。ただし、それには欠点もあります。まず、関数がすでに可能なすべての種類の値を返すことができる場合はどうなるでしょうか?そのような関数では、成功と失敗を区別できるように、結果をオブジェクトでラップするなどの処理を行う必要があります。これは、イテレーターインターフェイスのnextメソッドが行う方法と同様です。

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

特別な値を返すことの2番目の問題は、ぎこちないコードにつながる可能性があることです。あるコードがpromptNumberを10回呼び出す場合、nullが返されたかどうかを10回確認する必要があります。nullを見つけた場合の対応が、単にそれ自体がnullを返すことである場合、その関数の呼び出し元は、さらにそれを確認する必要があります。以下同様です。

例外

関数が正常に続行できない場合、私たちがやりたいことは、現在の作業を停止し、問題を処理する方法を知っている場所にすぐにジャンプすることです。これが例外処理が行うことです。

例外は、問題が発生したコードが例外を発生(またはスロー)できるようにするメカニズムです。例外は任意の値にすることができます。例外を発生させることは、関数からのスーパーチャージされた戻りにやや似ています。現在の関数だけでなく、呼び出し元からもジャンプし、現在の実行を開始した最初の呼び出しまで続きます。これはスタックの巻き戻しと呼ばれます。 第3章で言及した関数呼び出しのスタックを覚えているかもしれません。例外はこのスタックを駆け下り、遭遇したすべての呼び出しコンテキストを破棄します。

例外が常にスタックの一番下まで駆け下りるだけなら、あまり役に立たないでしょう。プログラムを破壊する斬新な方法を提供するだけです。その力は、スタックに沿ってキャッチするために「障害」を設定できるという事実に基づいています。例外をキャッチしたら、それを使用して問題に対処し、プログラムの実行を続行できます。

ここに例を示します。

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コンストラクターを使用して例外値を作成しました。これは、messageプロパティを持つオブジェクトを作成する標準の JavaScript コンストラクターです。 Errorのインスタンスは、例外が作成されたときに存在したコールスタックに関する情報(いわゆるスタックトレース)も収集します。この情報はstackプロパティに格納されており、問題のデバッグを試みるときに役立ちます。これにより、問題が発生した関数と、失敗した呼び出しを行った関数がわかります。

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

まあ、ほとんどの場合ですが...

例外後のクリーンアップ

例外の効果は、別の種類の制御フローです。例外を引き起こす可能性のあるすべてのアクション(ほとんどすべての関数呼び出しとプロパティアクセス)は、コードから突然制御を離れる可能性があります。

これは、コードに複数の副作用がある場合、たとえ「通常の」制御フローではすべてが必ず発生するように見えても、例外によってその一部が実行されなくなる可能性があることを意味します。

ここに、本当に悪い銀行のコードがあります。

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

function getAccount() {
  let accountName = prompt("Enter an account name");
  if (!Object.hasOwn(accounts, 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 を呼び出すなど、もう少し賢く書くことができました。しかし、このような問題は、より微妙な形で発生することがよくあります。例外をスローするように見えない関数でさえ、例外的な状況やプログラマーのミスがある場合には例外をスローする可能性があります。

これに対処する1つの方法は、副作用を減らすことです。ここでも、既存のデータを変更するのではなく、新しい値を計算するプログラミングスタイルが役立ちます。コードの実行が新しい値の作成途中で停止した場合でも、既存のデータ構造が損傷することはないため、復旧が容易になります。

常にそれが現実的であるとは限らないため、try ステートメントには別の機能があります。それは、catch ブロックの代わりに、またはそれに加えて、finally ブロックが続くことができるということです。finally ブロックは、「何が起こっても、try ブロックのコードを実行しようとした後に、このコードを実行する」ことを意味します。

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;
    }
  }
}

このバージョンの関数は、その進行状況を追跡し、終了時に、不整合なプログラム状態を作成した時点で中断されたことに気付いた場合、それが行った損害を修復します。

try ブロックで例外がスローされたときに finally コードが実行される場合でも、例外を妨げないことに注意してください。finally ブロックが実行された後、スタックの巻き戻しは継続されます。

予期しない場所で例外が発生した場合でも、確実に動作するプログラムを書くのは困難です。多くの人は単に気にしません。そして、例外は通常、例外的な状況のために予約されているため、問題が非常にまれに発生し、気づかれないことさえあります。それが良いことなのか、それとも本当に悪いことなのかは、ソフトウェアが失敗したときにどれだけの損害を与えるかによって異なります。

選択的なキャッチ

例外がキャッチされずにスタックの底まで到達した場合、環境によって処理されます。これが意味することは、環境によって異なります。ブラウザでは、通常、エラーの説明がJavaScriptコンソール(ブラウザのツールまたは開発者メニューからアクセス可能)に書き込まれます。第20章で説明するブラウザレスのJavaScript環境であるNode.jsは、データ破損についてより慎重です。未処理の例外が発生すると、プロセス全体を中止します。

プログラマーのミスについては、エラーをそのまま放置することが最善である場合があります。未処理の例外は、壊れたプログラムを知らせる合理的な方法であり、最新のブラウザでは、JavaScriptコンソールに、問題が発生したときにスタック上にあった関数呼び出しに関する情報が表示されます。

日常的な使用中に発生することが予想される問題については、未処理の例外でクラッシュさせるのはひどい戦略です。

存在しないバインディングを参照したり、nullのプロパティを検索したり、関数ではないものを呼び出したりするなど、言語の無効な使用も例外が発生する原因となります。このような例外もキャッチできます。

catch ブロックに入ったときに、私たちが知っているのは、try ブロックの何かが例外を引き起こしたということだけです。しかし、が引き起こしたのか、どの例外が引き起こされたのかはわかりません。

JavaScriptは(非常に明らかな省略で)例外を選択的にキャッチするための直接的なサポートを提供していません。すべてをキャッチするか、何もキャッチしないかのどちらかです。このため、catch ブロックを記述したときに考えていた例外であると想定したくなることがあります。

しかし、そうではない可能性があります。他の前提が侵害されているか、例外を引き起こしているバグを導入した可能性があります。これは、有効な答えが得られるまで promptDirection を呼び出し続けようとする例です。

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 ブロックは、例外値 (e) を完全に無視し、問題が何であるかを認識していると想定しているため、バインディングエラーを誤って不正な入力であると処理します。これは無限ループを引き起こすだけでなく、スペルミスのバインディングに関する有用なエラーメッセージを「埋もれさせて」しまいます。

原則として、例外を「ルーティング」する目的(たとえば、プログラムがクラッシュしたことを別のシステムに伝えるためにネットワーク経由でルーティングするなど)以外の場合は、例外を包括的にキャッチしないでください。たとえそうする場合でも、情報を隠してしまう可能性について慎重に検討してください。

特定の種類の例外をキャッチする必要があります。これを行うには、catch ブロックで、取得した例外が目的のものかどうかを確認し、そうでない場合は再スローします。しかし、例外をどのように認識すればよいのでしょうか。

その message プロパティを、予期されるエラーメッセージと比較することができます。しかし、それはコードを書くための不安定な方法です。プログラミング上の決定を行うために、人間が消費することを意図した情報(メッセージ)を使用することになります。誰かがメッセージを変更(または翻訳)するとすぐに、コードは動作しなくなります。

むしろ、新しいタイプのエラーを定義し、instanceof を使用して識別しましょう。

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 オブジェクトのように動作しますが、それらを認識できる異なるクラスを持っています。

これで、ループはこれらの例外をより慎重にキャッチできます。

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;
    }
  }
}

これは InputError のインスタンスのみをキャッチし、無関係な例外をそのままにします。スペルミスを再導入すると、未定義のバインディングエラーが適切に報告されます。

アサーション

アサーションは、プログラム内のチェックであり、何かが正しい状態であることを検証します。これらは、通常の操作で発生する可能性のある状況を処理するために使用されるのではなく、プログラマーのミスを見つけるために使用されます。

たとえば、firstElement が空の配列で呼び出してはいけない関数として記述されている場合、次のように記述できます。

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

これで、(存在しない配列プロパティを読み取るときに得られる)未定義を暗黙的に返す代わりに、誤って使用するとすぐにプログラムが大きくエラーを出力します。これにより、このようなミスが見過ごされる可能性が低くなり、発生した原因を見つけやすくなります。

考えられるすべての種類の不正な入力に対してアサーションを記述しようとすることはお勧めしません。それは大変な作業になり、非常にノイズの多いコードにつながります。それらは、犯しやすいミス(または自分が犯しているミス)のために取っておくのが良いでしょう。

まとめ

プログラミングの重要な部分として、バグの発見、診断、修正があります。自動テストスイートを使用したり、プログラムにアサーションを追加したりすると、問題に気づきやすくなります。

プログラムの制御外の要因によって引き起こされる問題は、通常、積極的に計画する必要があります。問題がローカルで処理できる場合は、特別な戻り値がそれらを追跡するのに適した方法となることがあります。それ以外の場合は、例外がより好ましいかもしれません。

例外をスローすると、次に外側のtry/catchブロック、またはスタックの底までコールスタックが巻き戻されます。例外の値は、それをキャッチするcatchブロックに渡されます。このブロックは、実際に予期される種類の例外であることを検証し、それに対して何らかの処理を行う必要があります。例外によって引き起こされる予測不可能な制御フローに対処するために、finallyブロックを使用して、ブロックが終了するときにコードが常に実行されるようにすることができます。

演習

再試行

20%のケースで2つの数値を乗算し、残りの80%のケースでMultiplicatorUnitFailure型の例外を発生させる関数primitiveMultiplyがあるとします。この扱いにくい関数をラップし、呼び出しが成功するまで試行し続け、成功したら結果を返す関数を作成してください。

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

class MultiplicatorUnitFailure extends Error {}

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

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

console.log(reliableMultiply(8, 8));
// → 64
ヒントを表示...

primitiveMultiplyの呼び出しは、必ずtryブロック内で行う必要があります。対応するcatchブロックは、例外がMultiplicatorUnitFailureのインスタンスでない場合は例外を再スローし、そうである場合は呼び出しが再試行されるようにする必要があります。

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

ロックされた箱

次の(やや作為的な)オブジェクトを考えてみましょう。

const box = new class {
  locked = true;
  #content = [];

  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};

これはロック付きの箱です。箱の中に配列がありますが、箱のロックが解除されている場合にのみアクセスできます。

withBoxUnlockedという名前の関数を作成してください。この関数は、関数値を引数として受け取り、箱のロックを解除し、関数を実行し、引数関数が正常に返されたか例外をスローしたかに関係なく、戻る前に箱が再びロックされていることを保証します。

const box = new class {
  locked = true;
  #content = [];

  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};

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

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

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

追加のポイントとして、箱がすでにロック解除されているときにwithBoxUnlockedを呼び出した場合、箱がロック解除されたままになるようにしてください。

ヒントを表示...

この演習では、finallyブロックが必要です。あなたの関数は、最初に箱のロックを解除し、try本体内から引数関数を呼び出す必要があります。その後のfinallyブロックは、箱を再びロックする必要があります。

箱がロックされていないときにロックしないようにするために、関数の開始時にロック状態を確認し、最初にロックされていた場合にのみロック解除およびロックを行うようにしてください。