全てが意図通りに働くプログラムを書くことが良いスタートです。そして予期しない状況に陥った時でもプログラムが正しく動くようにすることは困難ですがやりがいのあることです。
プログラムが陥りやすい厄介な状況は 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) はどうでしょう? この関数は分数指数を扱うことはできませんが、数学的に言えば、数を 1/2 乗することは全く妥当です (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);
}
文字列中に start と end が存在しない場合、indexOf は -1 を返し、この between が返す答えはナンセンスなものとなります: between("Your mother!", "{-", "-}") では "our mother" が返されます。
プログラムの実行中、コードは返される一連の値で問題なく処理が進むことを期待して関数をコールします。しかし値が間違っていれば、その値で処理した結果は間違いです。さらにその間違いは多くの関数に渡され、問題を引き起こします。こうなると、問題が何処で始まったかを突き止めるのが非常に困難になります。
正しくない入力で関数が間違った処理をしていることに気付かない場合があります。例えば、関数がコールされる場所が限定でき、そこでの入力が正しいと証明できる場合、問題が起こっても対処できるため、関数を大きくグロテスクなものにしてしまう原因にはなりません。
しかし多くの場合、この '無言で' 機能しなくなる関数は使いづらく、危険でさえあります。between をコールしたコードが処理が上手く行われたかを知りたい場合はどうでしょう? 当面の間、between が行った処理をやり直してその結果を失敗した between の結果とを見比べる以外、それを知ることはできません。これでは良くありません。一つの解決方法として、失敗した時に between に特別な値、false または undefined を返させるほうほうがあります。
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 が返す値をみると、そうとも言えません。
特別な値を返すことの次の問題は、非常に乱雑になってしまうことです。コードの一部が 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 をセットしたい、そしてその特定のものを他の関数でもアクセスできるようにしたいとします。通常、そのものに引数を渡すことを考えるでしょう。しかしこれは実用的ではありません。関数が終了すると、currentThing は元の null にセットされてしまうからです。
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 へのコールは例外によってスタックから捨てられ、currentThing は null にリセットされません。
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 章 で述べる新しいタイプのオブジェクトを導入することのほうが良いでしょう。