2024年6月23日

エラーハンドリング, "try..catch"

どんなに我々のプログラミングが素晴らしくても、スクリプトがエラーになることはあります。それはミス、予期しないユーザ入力、間違ったサーバレスポンスやその他多くの理由により発生する可能性があります。

通常、エラーが発生するとスクリプトは “死” (即時に停止) に、それをコンソールに出力します。

しかし、エラーを “キャッチ” し、死ぬ代わりにより意味のあることをする構文構造 try..catch があります。

“try…catch” 構文

try..catch 構造は2つのメインブロックを持っています: trycatch です。:

try {

  // code...

} catch (err) {

  // error handling

}

それは次のように動作します:

  1. まず、try {...} のコードが実行されます。
  2. エラーがなければ、catch(err) は無視されます: 実行が try の最後に到達した後、catch を飛び越えます。
  3. エラーが発生した場合、try の実行が停止し、コントロールフローは catch(err) の先頭になります。err 変数(任意の名前が使えます)は発生した事象に関する詳細をもつエラーオブジェクトを含んでいます。

従って、try {…} ブロックの内側のエラーはスクリプトを殺しません: catch の中でそれを扱う機会が持てます。

いくつか例を見てみましょう。

  • エラーなしの例: alert (1)(2) を表示します:

    try {
    
      alert('Start of try runs');  // (1) <--
    
      // ...ここではエラーはありません
    
      alert('End of try runs');   // (2) <--
    
    } catch(err) {
    
      alert('Catch is ignored, because there are no errors'); // (3)
    
    }
  • エラーの例: (1)(3) を表示します:

    try {
    
      alert('Start of try runs');  // (1) <--
    
      lalala; // エラー, 変数は宣言されていません!
    
      alert('End of try (never reached)');  // (2)
    
    } catch(err) {
    
      alert(`Error has occured!`); // (3) <--
    
    }
try..catch は実行時エラーにのみ作用します

try..catch を動作させるために、コードは実行可能でなければなりません。つまり、有効なJavaScriptである必要があります。

もしコードが構文的に誤っている場合には動作しません。例えば次は角括弧の不一致です:

try {
  {{{{{{{{{{{{
} catch(e) {
  alert("The engine can't understand this code, it's invalid");
}

JavaScriptエンジンは最初にコードを読み、次にそれを実行します。読み込みのフェーズで発生したエラーは “解析時間(parse-time)” エラーと呼ばれ、回復不能です(コードの内部からは)。なぜなら、エンジンはそのコードを理解することができないからです。

そのため、try..catch は有効なコードの中で起きたエラーのみを扱うことができます。このようなエラーは “ランタイムエラー” または “例外” と呼ばれます。

try..catch は同期的に動作します

もし setTimeout の中のような “スケジュールされた” コードで例外が発生した場合、try..catch はそれをキャッチしません。:

try {
  setTimeout(function() {
    noSuchVariable; // スクリプトはここで死にます
  }, 1000);
} catch (e) {
  alert( "won't work" );
}

try..catch は実際には関数をスケジュールする setTimeout 呼び出しをラップするためです。しかし関数自身は後で実行され、その時エンジンはすでに try..catch 構造を抜けています。

スケジュールされた関数の内側の例外をキャッチするためには、その関数の中に try..catch が必要です。:

setTimeout(function() {
  try {
    noSuchVariable; // try..catch がエラーをハンドリングします!
  } catch (e) {
    alert( "error is caught here!" );
  }
}, 1000);

エラーオブジェクト

エラーが発生したとき、JavaScript はその詳細を含めたオブジェクトを生成します。そして catch の引数として渡されます。:

try {
  // ...
} catch(err) { // <-- "エラーオブジェクト", err の代わりに別の名前を使うこともできます
  // ...
}

すべての組み込みのエラーに対して、catch ブロック内のエラーオブジェクトは2つの主なプロパティを持っています。:

name
エラー名です。未定義変数の場合、それは "ReferenceError" です。
message
エラー詳細に関するテキストメッセージです。

ほとんどの環境では、その他非標準のプロパティが利用可能です。最も広く使われ、サポートされているのは以下です:

stack
現在のコールスタックです: エラーに繋がったネスト呼び出しのシーケンスに関する情報を持つ文字列です。デバッグ目的で使われます。

例:

try {
  lalala; // エラー, 変数が宣言されていません!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...スタック呼び出し)

  // 全体としてエラーを表示する事もできます
  // エラーは "name: message" として文字列に変換されます
  alert(err); // ReferenceError: lalala is not defined
}

任意の “catch” バインディング

A recent addition
This is a recent addition to the language. Old browsers may need polyfills.

エラーの詳細が必要ない場合、catch はそれを省略できます:

try {
  // ...
} catch { // <--  (err) なし
  // ...
}

“try…catch” の利用

try..catch の実際のユースケースについて探索してみましょう。

既にご存知の通り、JavaScriptは JSONエンコードされた値を読むためのメソッド JSON.parse(str) がサポートされています。

通常、それはネットワーク経由でサーバまたは別のソースから受信したデータをデコードするために使われます。

今、次のようにデータを受信し、JSON.parse を呼び出します。:

let json = '{"name":"John", "age": 30}'; // サーバからのデータ

let user = JSON.parse(json); // テキスト表現をJSオブジェクトに変換

// 今、 user 文字列からプロパティを持つオブジェクトです
alert( user.name ); // John
alert( user.age );  // 30

JSON に関する詳細な情報は、チャプター JSON メソッド, toJSON を参照してください。

json が不正な形式の場合、JSON.parse はエラーになるのでスクリプトは “死にます”。

それで満足しますか?もちろん満足しません!

この方法だと、もしデータが何か間違っている場合、訪問者はそれを知ることができません(開発者コンソールを開かない限り)。また、人々は、エラーメッセージなしで何かが “単に死んでいる” ことを本当に本当に嫌います。

エラーを扱うために try..catch を使いましょう。:

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- エラーが起きたとき...
  alert( user.name ); // 動作しません

} catch (e) {
  // ...実行はここに飛びます
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( e.name );
  alert( e.message );
}

ここでは、メッセージを表示するためのだけに catch ブロックを使っていますが、より多くのことをすることができます。: 新たなネットワーク要求、訪問者への代替手段の提案、ロギング機構へエラーに関する情報の送信… すべて、単に死ぬよりははるかに良いです。

独自のエラーをスローする

仮に json が構文的に正しいが、必須の "name" プロパティを持っていない場合どうなるでしょう?

このように:

let json = '{ "age": 30 }'; // 不完全なデータ

try {

  let user = JSON.parse(json); // <-- エラーなし
  alert( user.name ); // name はありません!

} catch (e) {
  alert( "doesn't execute" );
}

ここで、JSON.parse は通常どおり実行しますが、"name" の欠落は実際には我々にとってはエラーです。

エラー処理を統一するために、throw 演算子を使います。

“Throw” 演算子

throw 演算子はエラーを生成します。

構文は次の通りです:

throw <error object>

技術的には、エラーオブジェクトとしてなんでも使うことができます。たとえ、数値や文字列のようなプリミティブでもOKです。しかし、namemessage プロパティを持つオブジェクトを使うのがベターです(組み込みのエラーと互換性をいくらか保つために)。

JavaScriptは標準エラーのための多くの組み込みのコンストラクタを持っています: Error, SyntaxError, ReferenceError, TypeError などです。我々もエラーオブジェクトを作るのにそれらが使えます。

構文は次の通りです:

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

組み込みのエラー(任意のオブジェクトではなく、エラーのみ)では、name プロパティはコンストラクタの名前と全く同じになります。そして message は引数から取られます。

例:

let error = new Error("Things happen o_O");

alert(error.name); // Error
alert(error.message); // Things happen o_O

JSON.parse が生成するエラーの種類を見てみましょう:

try {
  JSON.parse("{ bad json o_O }");
} catch(e) {
  alert(e.name); // SyntaxError
  alert(e.message); // Unexpected token o in JSON at position 0
}

ご覧の通り、それは SyntaxError です。

…そして、我々のケースでは、ユーザは必ず "name" を持っていると仮定するので、name の欠落もまた構文エラーとして扱います。

なので、それをスローするようにしましょう:

let json = '{ "age": 30 }'; // 不完全なデータ

try {

  let user = JSON.parse(json); // <-- エラーなし

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name"); // (*)
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}

(*) で、throw 演算子が与えられた messageSyntaxError を生成します。それは JavaScript が生成するのと同じ方法です。try の実行はすぐに停止し、制御フローは catch に移ります。

今や、catchJSON.parse と他のケースすべてのエラーハンドリングのための1つの場所になりました。

再スロー

上の例で、私たちは不正なデータを処理するために try..catch を使っています。しかし、try {...} ブロックの中で 別の予期しないエラー が発生する可能性はあるでしょうか? 変数が未定義、またはその他、単に “不正なデータ” ではない何か。

例:

let json = '{ "age": 30 }'; // 不完全なデータ

try {
  user = JSON.parse(json); // <-- user の前に "let" をつけ忘れた

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (実際にはJSONのエラーではありません)
}

もちろん、すべての可能性があります! プログラマはミスをするものです。何十年も何百万人もの人が使っているオープンソースのユーティリティであっても、突然酷いバグが発見され、ひどいハッキングにつながることがあります( ssh ツールで起こったようなものです)。

私たちのケースでは、try..catch は “不正なデータ” エラーをキャッチすることを意図しています。しかし、その性質上、catchtry からの すべての エラーを取得します。ここでは予期しないエラーが発生しますが、同じ "JSON Error" メッセージが表示されます。これは誤りでコードのデバッグをより難しくします。

このような問題を避けるため、“再スロー” という手法が利用できます。ルールはシンプルです:

キャッチはそれが知っているエラーだけを処理し、すべてのオブジェクトを “再スロー” するべきです

“再スロー” テクニックの詳細は次のように説明できます:

  1. すべてのエラーをキャッチします。
  2. catch(err) {...} ブロックで、エラーオブジェクト err を解析します。
  3. どう処理すればいいか分からなければ、throw err をします。

通常は、instanceof 演算子を使用してエラーの種類がチェックできます。:

try {
  user = { /*...*/ };
} catch (err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // 未定義変数へのアクセスに対する "ReferenceError"
  }
}

また、erro.name プロパティから、エラークラス名を取得することも可能です。すべてのネイティブエラーはエラークラス名があります。もう1つの選択肢は、err.constructor.name を参照することです。

下のコードでは、catchSyntaxError だけを処理するよう再スローを使っています。:

let json = '{ "age": 30 }'; // 不完全なデータ
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // 予期しないエラー

  alert( user.name );

} catch(e) {

  if (e.name == "SyntaxError") {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // 再スロー (*)
  }

}

(*) での、catch ブロック内部からのエラーのスローは try..catch を “抜けて” 外部の try..catch 構造(存在する場合)でキャッチされる、またはスクリプトをキルします。

従って、catch ブロックは実際に扱い方を知っているエラーだけを処理しその他すべてを “スキップ” します。

下の例は、このようなエラーが1つ上のレベルの try..catch で捕捉されるデモです:

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // error!
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
      throw e; // 再スロー (今のエラーの扱い方を知らない)
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // caught it!
}

ここでは、readDataSyntaxError の処理の仕方だけ知っており、外部の try..catch はすべての処理の方法を知っています。

try…catch…finally

待ってください、それですべてではありません。

try..catch 構造はもう1つのコード句: finally を持つ場合があります。

もし存在する場合、それはすべてのケースで実行します。:

  • エラーが無かった場合は、try の後で。
  • エラーがあった場合には catch の後で。

拡張された構文はこのようになります。:

try {
   ... コードを実行しようとします ...
} catch(e) {
   ... エラーを処理します ...
} finally {
   ... 常に実行します ...
}

このコードを実行してみましょう。:

try {
  alert( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

このコードは2つの実行方法があります。:

  1. もし “Make an error” に “Yes” と答えると、try -> catch -> finally となります。
  2. もし “No” と言えば、try -> finally となります。

finally 句は try..catch の前に何かを開始して、どのような結果であれファイナライズをしたいときに頻繁に使われます。

例えば、フィボナッチ数関数 fib(n) にかかる時間を計測したいとします。当然ながら、それを実行する前に計測を開始して、実行後に終了させることができます。しかし、仮に関数呼び出しの間でエラーが起きたらどうなるでしょう?特に下のコードの fib(n) の実装では、負の値または非整数値だとエラーを返します。

finally 句は何があっても計測を完了させるのに良い場所です。

ここで、finally は両方のシチュエーション – fib の実行が成功するケースと失敗するケース – で時間が正しく計測されることを保証します。:

let num = +prompt("Enter a positive integer number?", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("Must not be negative, and also an integer.");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (e) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "error occured");

alert( `execution took ${diff}ms` );

コードを実行して prompt35 を入力することで確認できます – 通常 try の後に finally を実行します。そして -1 を入れると – すぐにエラーになり、その実行は 0ms となります。両方の計測は正しく行われています。

つまり、関数を終了するには方法が2つあります: return または throw です。 finally 句はそれら両方とも処理します。

変数は try..catch..finally の内部でローカルです

上のコードで resultdiff 変数は try..catch で宣言されていることに注意してください。

そうでなく、let{...} ブロックの中で作られている場合、その中でしか見えません。

finallyreturn

Finally 句は try..catch からの 任意の 終了に対して機能します。それは明白な return も含みます。

下の例では、try の中で return があります。この場合、finally は制御が外部コードに戻る前に実行されます。

function func() {

  try {
    return 1;

  } catch (e) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // 最初に finally の alert が動作し、次にこれが動作します
try..finally

catch 句がない try..catch 構造も役立ちます。私たちはここでエラーを正しく処理したくないが、開始した処理が完了したことを確認したいときに使います。

function func() {
  // (計測など)完了させる必要のあるなにかを開始する
  try {
    // ...
  } finally {
    // すべてが死んでいても完了させる
  }
}

上のコードでは、try の内側のエラーは常に抜けます。なぜなら catch がないからです。しかし finally は実行フローが外部に移る前に機能します。

グローバルな catch

環境特有

このセクションの情報はコアなJavaScriptの一部ではありません。

try..catch の外側で致命的なエラーが起きてスクリプトが死んだことをイメージしてください。プログラミングエラーやその他何か酷いものによって。

そのような出来事に反応する方法はありますか? エラーをログに記録したり、ユーザーに何かを見せたり(通常はエラーメッセージが表示されません)。

仕様ではそのようなものはありませんが、通常、環境がそれを提供しています。なぜなら本当に有用だからです。例えば、Node.js はそのために process.on(‘uncaughtException’)を持っています。また、ブラウザでは関数を特別な window.onerror プロパティに代入することができます。それはキャッチしていないエラーの場合に実行されます。

構文:

window.onerror = function(message, url, line, col, error) {
  // ...
};
message
エラーメッセージ
url
エラーが起きたスクリプトのURL
line, col
エラーが起きた行と列番号
error
エラーオブジェクト

例:

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // おっと、何かがおかしいです!
  }

  readData();
</script>

グローバルハンドラー window.onerror の役割は、通常スクリプトの実行の回復ではありません – プログラミングエラーの場合、恐らくそれは不可能なので開発者にエラーメッセージを送ります。

このようなケースでエラーログを提供する web サービスもあります。https://errorception.com> や http://www.muscula.com

それらは次のように動きます:

  1. 私たちはサービスに登録し、ページに挿入するためのJSのピース(またはスクリプトのURL)をそれらから得ます。
  2. そのJSスクリプトはカスタムの window.onerror 関数を持っています。
  3. エラーが起きた時、そのサービスへネットワークリクエストを送ります。
  4. 私たちはサービスのWebインタフェースにログインしてエラーを見ることができます。

サマリ

try..catch 構造はランタイムエラーを処理することができます。文字通りコードを実行しようと試みて、その中で起こるエラーをキャッチします。

構文は次の通りです:

try {
  // コードを実行
} catch(err) {
  // エラーが起きた場合、ここにジャンプ
  // err はエラーオブジェクト
} finally {
  // すべてのケースで try/catch 後に実行する
}

catch セクションがない、または finally がない場合があります。なので try..catchtry..finally もまた有効です。

エラーオブジェクトは次のプロパティを持っています。:

  • message – 人が読めるエラーメッセージです。
  • name – エラー名を指す文字列です(エラーコンストラクタ名)
  • stack (非標準) – エラー生成時のスタックです。

エラーオブジェクトが不要であれば、catch (err) { の代わりに catch { とすることで省略できます。

また、throw 演算子を使って独自のエラーを生成することもできます。技術的には、throw の引数は何でもよいですが、通常は組み込みの Error クラスを継承しているエラーオブジェクトです。次のチャプターでエラーを拡張する方法について詳しく説明します。

再スロー はエラーハンドリングの非常に重要なパターンです。: catch ブロックは通常、特定のエラータイプを処理する方法を予期し、知っています。したがって、知らないエラーは再スローすべきです。

たとえ try..catch を持っていない場合でも、ほとんどの環境では “抜け出た” エラーをキャッチするために “グローバル” なエラーハンドラを設定することができます。ブラウザでは、それは window.onerror です。

タスク

重要性: 5

2つのコードの断片を比較してみてください。

  1. 1つ目は try..catch のあとにコードを実行するために finally を使います:

    try {
      work work
    } catch (e) {
      handle errors
    } finally {
      作業場所のクリーンアップ
    }
  2. 2つ目は try..catch の直後にクリーンアップする処理を置きます:

    try {
      work work
    } catch (e) {
      handle errors
    }
    
    作業場所のクリーンアップ

私たちは、処理が開始された後には、それがエラーかどうかは関係なく必ずクリーンアップが必要です。

finally を使うことの利点はあるでしょうか?それとも両方のコードは同じでしょうか?もし利点がある場合はそれが関係する例を挙げてください。

その違いは、関数内のコードを見ると明らかになります。

もし try..catch の “飛び出し” がある場合、振る舞いは異なります。

例えば、try..catch の中で return がある場合です。finally 句は try..catchどのような終わり方の場合にでも 動作します。たとえ、return 文経由でさえも。

function f() {
  try {
    alert('start');
    return "result";
  } catch (e) {
    /// ...
  } finally {
    alert('cleanup!');
  }
}

f(); // cleanup!

…もしくは次のように throw がある場合:

function f() {
  try {
    alert('start');
    throw new Error("an error");
  } catch (e) {
    // ...
    if("can't handle the error") {
      throw e;
    }

  } finally {
    alert('cleanup!')
  }
}

f(); // cleanup!

ここで finally はクリーンアップを保証します。もし f の終わりにコードをおいた場合は実行されない場合があります。

チュートリアルマップ