例外処理

この章ではJavaScriptにおける例外処理について学びます。

try...catch構文

try...catch構文は例外が発生しうるブロックをマークし、例外が発生したときの処理を記述するための構文です。 次の例のように、try文にはひとつのtryブロックがあり、tryブロック内で発生した例外をcatch節でキャッチします。

tryブロック内で例外が発生すると、それ以降の文は実行されずcatch節に処理が移ります。 finally節が存在するときには、例外がなげられたかどうかにかかわらず、かならずtry文の最後に実行されます。

try {
    console.log("この行は実行されます");
    // 未定義の関数を呼び出してReferenceError例外が発生する
    undefinedFunction();
    // 例外が発生したため、この行は実行されません
} catch (error) {
    // 例外が発生したあとはこのブロックが実行される
    console.log("この行は実行されます");
    console.log(error instanceof ReferenceError); // => true
    console.log(error.message); // => "undefinedFunction is not defined"
} finally {
    // このブロックはかならず実行される
    console.log("この行は実行されます");
}

また、catch節とfinally節のうち、片方が存在していれば、もう片方の節は省略できます。 finally節のみを書いた場合は例外がキャッチされないため、finally節を実行後に例外が発生します。

// catch節のみ
try {
    undefinedFunction();
} catch (error) {
    console.log(error);
}
// finally節のみ
try {
    undefinedFunction();
} finally {
    console.log("この行は実行されます");
}
// finally節のみでは例外がキャッチされないため、この行は実行されません

throw文

throw文を使うとユーザーが例外を投げることができます。 例外として投げられたオブジェクトは、catch節で関数の引数のようにアクセスできます。 catch節でオブジェクトを参照できる識別子を例外識別子と呼びます。

try {
    // 独自の例外を投げる
    throw new Error("例外が投げられました");
} catch (error) {
    // catch節のスコープでerrorにアクセスできる
    console.log(error.message); // => "例外が投げられました"
}

エラーオブジェクト

throw文ではエラーオブジェクトを例外として投げることができます。 ここでは、throw文で例外として投げられるエラーオブジェクトについて見ていきます。

Error

ErrorオブジェクトのインスタンスはErrornewして作成します。 コンストラクタの第一引数には、エラーメッセージとなる文字列を渡します。 渡したエラーメッセージはError#messageプロパティに格納されます。

次のコードでは、assertPositiveNumber関数でエラーオブジェクトを作成し、例外としてthrowしています。 投げられたオブジェクトは、catch節の例外識別子から取得でき、エラーメッセージが確認できます。

// 渡された数値が0未満であれば例外を投げる関数
function assertPositiveNumber(num) {
    if (num < 0) {
        throw new Error(`${num} is not positive.`);
    }
}

try {
    // 0未満の値を渡しているので、関数が例外を投げる
    assertPositiveNumber(-1);
} catch (error) {
    console.log(error instanceof Error); // => true
    console.log(error.message); // => "-1 is not positive."
}

throw文はあらゆるオブジェクトを例外として投げられますが、基本的にErrorオブジェクトのインスタンスを投げることが推奨されます。 その理由は後述するスタックトレースのためです。 Errorオブジェクトはインスタンスの作成時に、そのインスタンスが作成されたファイル名や行数などのデバッグに役立つ情報をもっています。 文字列のようなErrorオブジェクトでないオブジェクトを投げてしまうと、スタックトレースが得られません。

// 文字列を例外として投げるアンチパターンの例
try {
    throw "例外が投げられました";
} catch (error) {
    console.log(error); // => "例外が投げられました"
}

ビルトインエラー

JavaScriptエンジンが投げる組み込みのエラーのことをビルトインエラーと呼びます。 ビルトインエラーとして投げられるエラーオブジェクトは、すべてErrorオブジェクトを継承したオブジェクトのインスタンスです。 そのため、ユーザーが定義したエラーと同じように例外処理できます。

ビルトインエラーはいくつか種類がありますが、ここでは代表的なものを紹介します。

ReferenceError

ReferenceErrorは存在しない変数や関数などの識別子が参照された場合のエラーです。 次のコードでは、存在しない変数を参照しているためReferenceError例外が投げられます。

try {
    // 存在しない変数を参照する
    console.log(x);
} catch (error) {
    console.log(error instanceof ReferenceError); // => true
    console.log(error.name); // => "ReferenceError"
    console.log(error.message); // エラーメッセージが表示される
}

SyntaxError

SyntaxErrorは構文的に不正なコードを解釈しようとした場合のエラーです。 基本的にSyntaxError例外は、JavaScriptを実行する前のパース段階で発生します。 そのため、実行前に発生する例外であるSyntaxErrortry...catch文ではcatchできません。

// JavaScriptとして正しくない構文をパースするとSyntaxErrorが発生する
foo! bar!

次のコードでは、eval関数を使い実行時にSyntaxErrorを発生させています。 eval関数は渡した文字列をJavaScriptとして実行する関数です。 実行時に発生したSyntaxErrorは、try...catch文でもcatchできます。

try {
    // eval関数は渡した文字列をJavaScriptとして実行する関数
    // 正しくない構文をパースさせ、SyntaxErrorを実行時に発生させる
    eval("foo! bar!");
} catch (error) {
    console.log(error instanceof SyntaxError); // => true
    console.log(error.name); // => "SyntaxError"
    console.log(error.message); // エラーメッセージが表示される
}

TypeError

TypeErrorは値が期待される型でない場合のエラーです。 次のコードでは、関数ではないオブジェクトを関数呼び出ししているため、TypeError例外が投げられます。

try {
    // 関数でないオブジェクトを関数として呼び出す
    const fn = {};
    fn();
} catch (error) {
    console.log(error instanceof TypeError); // => true
    console.log(error.name); // => "TypeError"
    console.log(error.message); // エラーメッセージが表示される
}

ビルトインエラーを投げる

ビルトインエラーのインスタンスを作成し、そのインスタンスを例外として投げることもできます。 通常のErrorオブジェクトと同じように、それぞれのビルトインエラーオブジェクトをnewしてインスタンスを作成できます。

たとえば関数の引数を文字列に限定したい場合は、次のようにTypeError例外を投げるとよいでしょう。 メッセージを確認しなくても、エラーの名前だけで型に関する例外だとすぐにわかります。

// 文字列を反転する関数
function reverseString(str) {
    if (typeof str !== "string") {
        throw new TypeError(`${str} is not a string`);
    }
    return Array.from(str).reverse().join("");
}

try {
    // 数値を渡す
    reverseString(100);
} catch (error) {
    console.log(error instanceof TypeError); // => true
    console.log(error.name); // => "TypeError"
    console.log(error.message); // "100 is not a string"
}

エラーとデバッグ

JavaScript開発においてデバッグ中に発生したエラーを理解することは非常に重要です。 エラーがもつ情報を活用することで、ソースコードのどこでどのような例外が投げられたのか知ることができます。

エラーはすべてErrorオブジェクトを拡張したオブジェクトで宣言されています。 つまり、エラーの名前をあらわすnameプロパティと内容をあらわすmessageプロパティをもっています。 この2つのプロパティを確認することで、多くの場面で開発の助けとなるでしょう。

次のコードでは、try...catch文で囲っていない部分で例外が発生しています。

function fn() {
    // 存在しない変数を参照する
    x++;
}
fn();

このスクリプトを読み込むと、投げられた例外についてのログがコンソールに出力されます。 ここではFirefoxにおける実行例を示します。

コンソールでのエラー表示(Firefox)

このエラーログには次の情報が含まれています。

メッセージ 意味
ReferenceError: x is not defined エラーの種類はReferenceErrorで、xが未定義であること
error.js:3:5 例外がerror.jsの3行目5列目で発生したこと。つまりx++;であること。

また、メッセージの後には例外のスタックトレースが表示されています。 スタックトレースとは、プログラムの実行過程を記録した内容で、どの処理によってエラーが発生したかが書かれています。

  • スタックトレースの最初の行が実際に例外が発生した場所です。つまり、3行目の x++; で例外が発生しています
  • 次の行には、そのコードの呼び出し元が記録されています。つまり、3行目のコードを実行したのは5行目のfn関数の呼び出しです

このように、スタックトレースは上から下へ呼び出し元を辿れるように記録されています。

コンソールに表示されるエラーログには多くの情報が含まれています。 MDNのJavaScriptエラーリファレンスには、ブラウザが投げるビルトインのエラーについて種類とメッセージが網羅されています。 開発中にビルトインエラーが発生したときには、リファレンスを見て解決方法を探すとよいでしょう。

console.errorとスタックトレース

console.errorメソッドはメッセージと合わせてスタックトレースをコンソールへ出力できます。

次のコードを実行して、console.logconsole.errorの出力結果を見比べてみます。

function fn() {
    console.log("メッセージ");
    console.error("エラーメッセージ");
}

fn();

このコードをFirefoxで実行するとコンソール出力は次の図のようになります。

console.logとconsole.errorの出力結果

console.logはメッセージだけなのに対して、console.errorではメッセージと共にスタックトレースが出力されます。 そのため、エラーが発生した場合のコンソールへのメッセージ出力にconsole.errorを利用することでデバッグがしやすくなります。

また、ほとんどのブラウザにはconsole.logconsole.errorの出力をフィルタリングできる機能が備わっています。 ただのログ出力はconsole.logを使い、エラーに関するログ出力はconsole.errorと使い分けることで、ログの重要度が区別しやすくなります。