第5章: エラー処理

すべてが期待通りに進む場合に動作するプログラムを書くことは良いスタートです。予期しない状況に遭遇した場合にプログラムを適切に動作させることが、真の課題となります。

プログラムが遭遇する可能性のある問題のある状況は、プログラマーのミスと真の問題の2つのカテゴリに分類されます。関数の必須引数を渡すのを忘れた場合、それは最初の種類の問題の例です。一方、プログラムがユーザーに名前を入力するように要求し、空の文字列が返された場合、それはプログラマーが防ぐことができないものです。

一般的に、プログラマーエラーはそれらを見つけて修正することで対処し、真のエラーはコードにそれらをチェックさせ、それらを修正するための適切なアクションを実行する(たとえば、名前を再度要求する)、または少なくとも明確に定義されたクリーンな方法で失敗することで対処します。


特定の問題がこれらのカテゴリのどちらに該当するかを判断することが重要です。たとえば、以前のpower関数について考えてみましょう。

function power(base, exponent) {
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

あるギークがpower("Rabbit", 4)を呼び出そうとすると、それは明らかにプログラマーエラーですが、power(9, 0.5)はどうでしょうか?この関数は小数点以下の指数を処理できませんが、数学的には、数値を2分の1乗することは完全に合理的です(Math.powはそれを処理できます)。関数がどのような入力を受け入れるかが完全に明確でない状況では、受け入れ可能な引数の種類をコメントで明示的に記述することをお勧めします。


関数が自身で解決できない問題に遭遇した場合、どうすればよいでしょうか?第4章では、between関数を作成しました。

function between(string, start, end) {
  var startAt = string.indexOf(start) + start.length;
  var endAt = string.indexOf(end, startAt);
  return string.slice(startAt, endAt);
}

指定されたstartendが文字列に存在しない場合、indexOf-1を返し、このバージョンのbetweenは多くのナンセンスを返します。between("Your mother!", "{-", "-}")"our mother"を返します。

プログラムが実行されていて、関数がそのように呼び出されると、それを呼び出したコードは期待どおりに文字列値を取得し、それで何かを喜んで続けます。しかし、値は間違っているため、最終的にそれで何をするにしても間違っています。そして、運が悪いと、この誤りは20個の他の関数を通過した後にのみ問題を引き起こします。このような場合、問題がどこで始まったのかを見つけるのは非常に困難です。

場合によっては、これらの問題をそれほど気にせず、間違った入力が与えられたときに関数が誤動作しても気にしないことがあります。たとえば、関数が少数の場所からのみ呼び出されることが確実であり、これらの場所が適切な入力を提供することを証明できる場合、問題のあるケースを処理するために関数を大きくしたり、醜くしたりする手間をかける価値は一般的にありません。

しかし、ほとんどの場合、「サイレントに」失敗する関数は使いにくく、危険でさえあります。betweenを呼び出すコードがすべてうまくいったかどうかを知りたい場合はどうでしょうか?現時点では、betweenが行ったすべての作業をやり直して、betweenの結果を独自の結果と照合することによってのみ判断できます。それは良くありません。1つの解決策は、betweenが失敗したときに、falseundefinedなどの特別な値を返すようにすることです。

function between(string, start, end) {
  var startAt = string.indexOf(start);
  if (startAt == -1)
    return undefined;
  startAt += start.length;
  var endAt = string.indexOf(end, startAt);
  if (endAt == -1)
    return undefined;

  return string.slice(startAt, endAt);
}

エラーチェックは一般的に関数をきれいにするわけではないことがわかります。しかし、 अब between を呼ぶコードは次のようなことができます.

var input = prompt("Tell me something", "");
var parenthesized = between(input, "(", ")");
if (parenthesized != undefined)
  print("You parenthesized '", parenthesized, "'.");

多くの場合、特別な値を返すことは、エラーを示すのに最適な方法です。ただし、欠点もあります。第一に、関数がすでにあらゆる種類の値を返すことができる場合はどうでしょうか?たとえば、配列から最後の要素を取得するこの関数を考えてみましょう。

function lastElement(array) {
  if (array.length > 0)
    return array[array.length - 1];
  else
    return undefined;
}

show(lastElement([1, 2, undefined]));

では、配列には最後の要素がありましたか?lastElementが返す値を見ても、判断できません。

特別な値を返すことの2番目の問題は、それが雑然とした状態につながることがあるということです. コードの一部がbetweenを10回呼び出す場合、undefinedが返されたかどうかを10回チェックする必要があります。また、関数がbetweenを呼び出すが、障害から回復するための戦略がない場合、betweenの戻り値をチェックする必要があり、それがundefinedの場合、この関数は呼び出し元にundefinedまたはその他の特別な値を返すことができ、呼び出し元もこの値をチェックします。

奇妙なことが起こったとき、私たちがやっていることをやめて、問題の処理方法を知っている場所にすぐにジャンプバックできれば実用的です。

幸いなことに、多くのプログラミング言語はそのようなものを提供しています。通常、それは例外処理と呼ばれます。


例外処理の背後にある理論は次のとおりです。コードは発生させる(またはスローする)ことが可能です。例外、これは値です。例外を発生させることは、関数からのスーパーチャージされた戻り値にいくぶん似ています。現在の関数からジャンプアウトするだけでなく、現在の関数の実行を開始したトップレベルの呼び出しまで、呼び出し元からもジャンプアウトします。これはスタックのアンワインドと呼ばれます。第3章で言及された関数のスタックを覚えているかもしれません。例外はこのスタックをズームダウンし、遭遇したすべての呼び出しコンテキストを捨てます。

例外が常にスタックのベースまでズームダウンする場合、例外はあまり役に立ちません。プログラムを爆破する新しい方法を提供するだけです。幸いなことに、スタックに沿って例外の障害を設定することができます。これらは、ズームダウンするときに例外を「キャッチ」し、それを使って何かをすることができ、その後、プログラムは例外がキャッチされた場所で実行を続けます。

function lastElement(array) {
  if (array.length > 0)
    return array[array.length - 1];
  else
    throw "Can not take the last element of an empty array.";
}

function lastElementPlusTen(array) {
  return lastElement(array) + 10;
}

try {
  print(lastElementPlusTen([]));
}
catch (error) {
  print("Something went wrong: ", error);
}

throwは、例外を発生させるために使用されるキーワードです。キーワードtryは例外の障害を設定します。その後のブロック内のコードが例外を発生させると、catchブロックが実行されます。catchという単語の後の括弧内に名前が付けられた変数は、このブロック内で例外値に付けられた名前です。

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

まあ、ほとんど。


次の点を考えてみましょう。関数processThingは、本体の実行中にトップレベル変数currentThingを特定のものを指すように設定して、他の関数がそのものにアクセスできるようにしたいと考えています。通常は引数として渡すだけですが、それが実際的ではないと仮定します。関数が終了すると、currentThingnullにリセットされる必要があります。

var currentThing = null;

function processThing(thing) {
  if (currentThing != null)
    throw "Oh no! We are already processing a thing!";

  currentThing = thing;
  /* do complicated processing... */
  currentThing = null;
}

しかし、複雑な処理で例外が発生した場合はどうでしょうか?その場合、processThingの呼び出しは例外によってスタックからスローされ、currentThingnullにリセットされません。

tryステートメントの後にfinallyキーワードを続けることもできます。これは、「何が起こっても、tryブロック内のコードを実行しようとした後にこのコードを実行する」ことを意味します。関数が何かをクリーンアップする必要がある場合、クリーンアップコードは通常finallyブロックに配置する必要があります。

function processThing(thing) {
  if (currentThing != null)
    throw "Oh no! We are already processing a thing!";

  currentThing = thing;
  try {
    /* do complicated processing... */
  }
  finally {
    currentThing = null;
  }
}

プログラムの多くのエラーにより、JavaScript環境で例外が発生します。例えば

try {
  print(Sasquatch);
}
catch (error) {
  print("Caught: " + error.message);
}

このような場合、特別なエラーオブジェクトが発生します。これらには常に、問題の説明を含むmessageプロパティがあります。newキーワードとErrorコンストラクターを使用して、同様のオブジェクトを発生させることができます。

throw new Error("Fire!");

例外がキャッチされずにスタックの一番下まで行くと、環境によって処理されます。これは、ブラウザによって異なり、エラーの説明が何らかのログに書き込まれる場合と、エラーを説明するウィンドウがポップアップする場合があります。

このページのコンソールにコードを入力することによって生成されたエラーは、常にコンソールによってキャッチされ、他の出力の中に表示されます。


ほとんどのプログラマーは、例外を純粋にエラー処理メカニズムと考えています。しかし、本質的に、それらはプログラムの制御フローに影響を与える別の方法にすぎません。たとえば、再帰関数で一種のbreakステートメントとして使用できます。オブジェクトと、その中に格納されているオブジェクトに、少なくとも7つのtrue値が含まれているかどうかを判断する、少し奇妙な関数を次に示します.

var FoundSeven = {};

function hasSevenTruths(object) {
  var counted = 0;

  function count(object) {
    for (var name in object) {
      if (object[name] === true) {
        counted++;
        if (counted == 7)
          throw FoundSeven;
      }
      else if (typeof object[name] == "object") {
        count(object[name]);
      }
    }
  }

  try {
    count(object);
    return false;
  }
  catch (exception) {
    if (exception != FoundSeven)
      throw exception;
    return true;
  }
}

内部関数countは、引数の一部であるすべてのオブジェクトに対して再帰的に呼び出されます。変数countedが7に達すると、カウントを続ける意味はありませんが、countの現在の呼び出しから戻るだけでは、必ずしもカウントが停止するとは限りません。その下にさらに呼び出しがある可能性があるためです。したがって、私たちがすることは、値をスローすることだけです。これにより、制御はcountへの呼び出しからすぐにジャンプアウトし、catchブロックに着陸します。

しかし、例外が発生した場合に単に true を返すのは正しくありません。何か他の問題が発生している可能性があるため、最初に例外が、この目的のために特別に作成されたオブジェクト FoundSeven であるかどうかを確認します。そうでない場合、この catch ブロックはそれを処理する方法を知らないため、再び例外を発生させます。

これは、エラー状態を処理する場合にも一般的なパターンです。catch ブロックが、処理方法を知っている例外のみを処理するようにする必要があります。この章のいくつかの例で行っているように、文字列値をスローすることは、例外の種類を認識しにくくするため、めったに良い考えではありません。より良い方法は、FoundSeven オブジェクトのような一意の値を使用するか、第8章で説明されているように、新しいタイプのオブジェクトを導入することです。