第4版が利用可能です。ここで読む

第8章バグとエラー

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

Brian Kernighan and P.J. Plauger, プログラミング作法
Picture of a collection of bugs

コンピュータプログラムの欠陥は、通常、バグと呼ばれます。プログラマーは、バグをまるで小さな虫が私たちの仕事にたまたま入り込んでくるかのように想像すると、気分が良くなります。しかし実際には、もちろん、私たち自身がバグをそこに置いているのです。

プログラムが結晶化された思考である場合、バグは大まかに、思考が混乱していることによって引き起こされるものと、思考をコードに変換する際に発生するミスによって引き起こされるものの2つに分類できます。前者のタイプのバグは、一般的に後者のタイプよりも診断と修正が困難です。

言語

私たちが何をしようとしているのかをコンピュータが十分に理解していれば、多くのミスを自動的に指摘してもらうことができます。しかし、ここでは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

通常、例のように `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は、プログラムを実際に実行しているときのみ型を考慮し、そこでも多くの場合、値を期待される型に暗黙的に変換しようとするため、あまり役に立ちません。

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

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

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

JavaScriptプログラムに型で注釈を付けるためのさまざまな規則がいくつかあります。

型についての一つのことは、十分なコードを記述して役立つようにするために、独自の複雑さを導入する必要があるということです。配列からランダムな要素を返す `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`に`13`、`1`、そして`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`があるとします。ユーザーが「orange」と入力した場合、何を返す必要がありますか?

1つの選択肢は、特別な値を返すようにすることです。このような値の一般的な選択肢は、`null`、`undefined`、または-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`を呼び出すコードはすべて、実際に数値が読み取られたかどうかを確認し、そうでない場合は、何らかの方法で回復する必要があります。たとえば、もう一度質問するか、デフォルト値を入力します。または、失敗したことを*その*呼び出し元に示すために、特別な値を再び返すこともできます。

多くの状況、特にエラーが一般的であり、呼び出し元がエラーを明示的に考慮する必要がある場合、特別な値を返すことはエラーを示す良い方法です。ただし、欠点もあります。まず、関数が既にあらゆる種類の値を返すことができる場合はどうでしょうか?このような関数では、成功と失敗を区別できるように、結果をオブジェクトにラップするなどする必要があります。

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

特別な値を返すことの2つ目の問題は、ぎこちないコードにつながる可能性があることです。コードの一部が`promptNumber`を10回呼び出す場合、`null`が返されたかどうかを10回チェックする必要があります。そして、`null`を見つけた場合の応答が単に`null`自体を返すことであれば、関数の呼び出し元はそれをチェックする必要があり、というように続きます。

例外

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

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

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

まあ、ほとんど...

例外後のクリーンアップ

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

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

これは本当にひどい銀行コードです。

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`を呼び出すなど、もう少しインテリジェントに記述できた可能性があります。しかし、多くの場合、このような問題はより微妙な方法で発生します。例外をスローしないように見える関数でも、例外的な状況やプログラマーのミスが含まれている場合に例外をスローする可能性があります。

これに対処する1つの方法は、副作用を減らすことです。繰り返しになりますが、既存のデータを変更する代わりに新しい値を計算するプログラミングスタイルが役立ちます。新しい値の作成中にコードの実行が停止した場合、未完成の値を見る人はおらず、問題はありません。

しかし、これは常に実用的とは限りません。そのため、`try`文には別の機能があります。`catch`ブロックの代わりに、または`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 = {
  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`という関数を記述します。

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

追加課題として、ボックスが既にアンロックされている状態で `withBoxUnlocked` を呼び出した場合、ボックスがアンロックされたままになるようにしてください。

この演習では、`finally` ブロックが必要です。関数はまずボックスのロックを解除し、`try` ブロック内から引数として渡された関数を呼び出す必要があります。その後の `finally` ブロックで、ボックスを再びロックする必要があります。

元々ロックされていなかったボックスをロックしないようにするために、関数の開始時にボックスのロック状態を確認し、最初にロックされていた場合にのみ、ロック解除とロックを行うようにしてください。