2022年10月14日

エラー処理、"try...catch"

どれだけプログラミングが得意であっても、スクリプトにエラーが発生することはあります。エラーは、私たちの間違い、予期しないユーザー入力、誤ったサーバー応答など、さまざまな理由で発生する可能性があります。

通常、エラーが発生した場合、スクリプトは「停止」(すぐに停止)し、コンソールにエラーを出力します。

しかし、`try...catch` という構文構造体があり、これを使うことでエラーを「キャッチ」できるため、スクリプトは停止する代わりに、より合理的な処理を行うことができます。

「try…catch」構文

`try...catch` 構造体には、`try` と `catch` の2つの主要なブロックがあります。

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) <--
    
      // ...no errors here
    
      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; // error, variable is not defined!
    
      alert('End of try (never reached)');  // (2)
    
    } catch (err) {
    
      alert(`Error has occurred!`); // (3) <--
    
    }
`try...catch` は実行時エラーのみに有効です

`try...catch` が機能するためには、コードが実行可能でなければなりません。言い換えれば、有効な JavaScript でなければなりません。

コードの構文が間違っている場合、例えば中括弧の対応が取れていない場合、これは機能しません。

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

JavaScript エンジンは最初にコードを読み取り、次に実行します。読み取りフェーズで発生するエラーは「解析時」エラーと呼ばれ、(そのコード内からは)回復できません。これは、エンジンがコードを理解できないためです。

そのため、`try...catch` は有効なコードで発生するエラーのみを処理できます。このようなエラーは「実行時エラー」または「例外」と呼ばれることがあります。

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

`setTimeout` など、「スケジュールされた」コードで例外が発生した場合、`try...catch` はそれをキャッチしません。

try {
  setTimeout(function() {
    noSuchVariable; // script will die here
  }, 1000);
} catch (err) {
  alert( "won't work" );
}

これは、エンジンがすでに `try...catch` 構造体を離れた後に、関数自体が後で実行されるためです。

スケジュールされた関数内で例外をキャッチするには、`try...catch` をその関数内に配置する必要があります。

setTimeout(function() {
  try {
    noSuchVariable; // try...catch handles the error!
  } catch {
    alert( "error is caught here!" );
  }
}, 1000);

エラーオブジェクト

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

try {
  // ...
} catch (err) { // <-- the "error object", could use another word instead of err
  // ...
}

すべての組み込みエラーの場合、エラーオブジェクトには2つの主要なプロパティがあります。

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

ほとんどの環境では、他の非標準プロパティも利用できます。最も広く使用され、サポートされているものの1つは次のとおりです。

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

例えば

try {
  lalala; // error, variable is not defined!
} catch (err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)

  // Can also show an error as a whole
  // The error is converted to string as "name: message"
  alert(err); // ReferenceError: lalala is not defined
}

オプションの「catch」バインディング

最近の追加事項
これは言語への最近の追加事項です。古いブラウザではポリフィルが必要になる場合があります。

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

try {
  // ...
} catch { // <-- without (err)
  // ...
}

「try…catch」の使い方

`try...catch` の実際のユースケースを見てみましょう。

すでに知っているように、JavaScript は JSON エンコードされた値を読み取るために JSON.parse(str) メソッドをサポートしています。

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

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

let json = '{"name":"John", "age": 30}'; // data from the server

let user = JSON.parse(json); // convert the text representation to JS object

// now user is an object with properties from the string
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); // <-- when an error occurs...
  alert( user.name ); // doesn't work

} catch (err) {
  // ...the execution jumps here
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( err.name );
  alert( err.message );
}

ここでは、`catch` ブロックを使用してメッセージを表示するだけですが、さらに多くのことができます。新しいネットワークリクエストを送信したり、訪問者に代替案を提案したり、エラーに関する情報をログ記録機能に送信したりすることができます。ただ停止するよりもはるかに優れています。

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

`json` の構文は正しいが、必須の `name` プロパティがない場合はどうでしょうか?

このように

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors
  alert( user.name ); // no name!

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

ここでは `JSON.parse` は正常に実行されますが、`name` がないことは実際にはエラーです。

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

「throw」演算子

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

構文は次のとおりです。

throw <error object>

技術的には、エラーオブジェクトとして何でも使用できます。それは数値や文字列のようなプリミティブでさえありますが、オブジェクトを使用するのが better です。できれば `name` と `message` プロパティを持つオブジェクトを使用するのが better です(組み込みエラーとの互換性をある程度維持するため)。

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 (err) {
  alert(err.name); // SyntaxError
  alert(err.message); // Unexpected token b in JSON at position 2
}

ご覧のとおり、それは `SyntaxError` です。

そして私たちの場合、`name` がないことはエラーです。ユーザーは `name` を持っていなければなりません。

それでは、スローしてみましょう。

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors

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

  alert( user.name );

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

行 `(*)` では、`throw` 演算子は、JavaScript 自体が生成するのと同じ方法で、指定された `message` を使用して `SyntaxError` を生成します。`try` の実行はすぐに停止し、制御フローは `catch` にジャンプします。

これで `catch` は、`JSON.parse` と他のケースの両方で、すべてのエラー処理を行う単一の場所になりました。

再スロー

上記の例では、`try...catch` を使用して不正なデータを処理しています。しかし、`try {...}` ブロック内で*別の予期しないエラー*が発生する可能性はありますか?プログラミングエラー(変数が定義されていない)やその他の、この「不正なデータ」のことだけではない何か。

例えば

let json = '{ "age": 30 }'; // incomplete data

try {
  user = JSON.parse(json); // <-- forgot to put "let" before user

  // ...
} catch (err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (no JSON Error actually)
}

もちろん、すべてが可能です!プログラマーは間違いを犯します。何十年もの間、何百万もの人が使用しているオープンソースユーティリティでさえ、突然、恐ろしいハッキングにつながるバグが発見される可能性があります。

私たちの場合、`try...catch` は「不正なデータ」エラーをキャッチするために配置されています。しかし、その性質上、`catch` は `try` から*すべて*のエラーを取得します。ここでは予期しないエラーが発生しますが、それでも同じ `"JSON エラー"` メッセージが表示されます。これは間違っており、コードのデバッグをより困難にします。

このような問題を回避するために、「再スロー」テクニックを採用できます。ルールは簡単です。

Catch は、知っているエラーのみを処理し、他のすべてを「再スロー」する必要があります。

「再スロー」テクニックは、より詳細に次のように説明できます。

  1. Catch はすべてのエラーを取得します。
  2. `catch (err) {...}` ブロックで、エラーオブジェクト `err` を分析します。
  3. 処理方法がわからない場合は、`throw err` を実行します。

通常、`instanceof` 演算子を使用してエラータイプを確認できます。

try {
  user = { /*...*/ };
} catch (err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
  }
}

また、`err.name` プロパティからエラークラス名を取得することもできます。すべてのネイティブエラーはそれを持っています。別のオプションは、`err.constructor.name` を読み取ることです。

以下のコードでは、`catch` が `SyntaxError` のみを処理するように再スローを使用しています。

let json = '{ "age": 30 }'; // incomplete data
try {

  let user = JSON.parse(json);

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

  blabla(); // unexpected error

  alert( user.name );

} catch (err) {

  if (err instanceof SyntaxError) {
    alert( "JSON Error: " + err.message );
  } else {
    throw err; // rethrow (*)
  }

}

`catch` ブロック内からの行 `(*)` のエラースローは、`try...catch` から「抜け出し」、外部の `try...catch` 構造体(存在する場合)によってキャッチされるか、スクリプトを停止させる可能性があります。

そのため、`catch` ブロックは実際には、処理方法を知っているエラーのみを処理し、他のすべてを「スキップ」します。

以下の例は、そのようなエラーがさらに1レベルの `try...catch` によってどのようにキャッチされるかを示しています。

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

  try {
    // ...
    blabla(); // error!
  } catch (err) {
    // ...
    if (!(err instanceof SyntaxError)) {
      throw err; // rethrow (don't know how to deal with it)
    }
  }
}

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

ここでは、`readData` は `SyntaxError` の処理方法のみを知っており、外部の `try...catch` はすべてを処理する方法を知っています。

try…catch…finally

ちょっと待って、まだ終わりではありません。

`try...catch` 構造体には、もう1つのコード句があります。`finally` です。

存在する場合、それはすべての場合で実行されます。

  • エラーがなかった場合は、`try` の後。
  • エラーがあった場合は、`catch` の後。

拡張構文は次のようになります。

try {
   ... try to execute the code ...
} catch (err) {
   ... handle errors ...
} finally {
   ... execute always ...
}

このコードを実行してみてください。

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

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

  1. 「エラーを起こしますか?」に「はい」と答えると、`try -> catch -> finally` となります。
  2. 「いいえ」と答えると、`try -> finally` となります。

`finally` 句は、何かを始めたとき、結果がどうであれそれを完了したい場合によく使用されます。

たとえば、フィボナッチ数関数 `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 (err) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "error occurred");

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

プロンプトに `35` を入力してコードを実行することで確認できます。正常に実行され、`try` の後に `finally` が実行されます。次に `-1` を入力すると、すぐにエラーが発生し、実行には `0ms` かかります。どちらの測定も正しく行われています。

言い換えれば、関数は `return` または `throw` で終了する可能性がありますが、それは問題ではありません。`finally` 句はどちらの場合でも実行されます。

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

上記のコードの `result` 変数と `diff` 変数は、`try...catch` の*前に*宣言されていることに注意してください。

そうでない場合、`try` ブロックで `let` を宣言した場合、それはその内部でのみ表示されます。

`finally` と `return`

`finally` 句は、`try...catch` からの*任意*の終了に対して機能します。これには、明示的な `return` が含まれます。

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

function func() {

  try {
    return 1;

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

alert( func() ); // first works alert from finally, and then this one
try...finally

catch 節のない try...finally 構文も便利です。これは、ここでエラーを処理したくなく(そのままスルーさせたい)、しかし開始したプロセスが確実に終了するようにしたい場合に適用します。

function func() {
  // start doing something that needs completion (like measurements)
  try {
    // ...
  } finally {
    // complete that thing even if all dies
  }
}

上記のコードでは、catch がないため、try 内のエラーは常に外部に飛び出します。しかし、finally は実行フローが関数から抜ける前に動作します。

グローバルキャッチ

環境依存

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

try...catch の外側で致命的なエラーが発生し、スクリプトが停止したとします。プログラミングエラーやその他の深刻な問題などです。

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

仕様にはそのようなものはありませんが、非常に便利なので、環境は通常それを提供しています。たとえば、Node.js には、そのための process.on("uncaughtException") があります。ブラウザでは、特別な window.onerror プロパティに関数を割り当てることができます。これは、キャッチされないエラーが発生した場合に実行されます。

構文

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

例えば

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

  function readData() {
    badFunc(); // Whoops, something went wrong!
  }

  readData();
</script>

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

https://errorception.comhttps://www.muscula.com のような、このような場合のエラーロギングを提供する Web サービスもあります。

これらのサービスは次のように動作します

  1. サービスに登録し、ページに挿入するための JS の一部(またはスクリプト URL)を取得します。
  2. その JS スクリプトは、カスタム window.onerror 関数を設定します。
  3. エラーが発生すると、それに関するネットワークリクエストをサービスに送信します。
  4. サービスの Web インターフェースにログインして、エラーを確認できます。

まとめ

try...catch 構文を使用すると、ランタイムエラーを処理できます。文字通り、コードを実行してみて、発生する可能性のあるエラーを「キャッチ」することができます。

構文は次のとおりです。

try {
  // run this code
} catch (err) {
  // if an error happened, then jump here
  // err is the error object
} finally {
  // do in any case after try/catch
}

catch セクションまたは finally がない場合があるため、より短い構文 try...catch および try...finally も有効です。

エラーオブジェクトには、次のプロパティがあります

  • message - 人間が読めるエラーメッセージ。
  • name - エラー名(エラーコンストラクタ名)を含む文字列。
  • stack(非標準ですが、十分にサポートされています) - エラー発生時のスタック。

エラーオブジェクトが必要ない場合は、catch (err) { の代わりに catch { を使用して省略できます。

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

_再スロー_は、エラー処理の非常に重要なパターンです。catch ブロックは通常、特定のエラータイプを予期し、処理方法を知っているため、知らないエラーは再スローする必要があります。

try...catch がない場合でも、ほとんどの環境では、「グローバル」エラーハンドラを設定して、「フォールアウト」するエラーをキャッチできます。ブラウザでは、それは window.onerror です。

タスク

重要度:5

2 つのコードフラグメントを比較します。

  1. 最初のコードは、finally を使用して try...catch の後にコードを実行します

    try {
      work work
    } catch (err) {
      handle errors
    } finally {
      cleanup the working space
    }
  2. 2 番目のフラグメントは、try...catch の直後にクリーンアップを配置します

    try {
      work work
    } catch (err) {
      handle errors
    }
    
    cleanup the working space

作業の後には、エラーの有無にかかわらず、クリーンアップが必ず必要です。

ここで finally を使用することの利点はあるでしょうか、それとも両方のコードフラグメントは同等でしょうか?利点がある場合は、それが重要な場合の例を挙げてください。

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

try...catch から「ジャンプアウト」がある場合、動作は異なります。

たとえば、try...catch 内に return がある場合。finally 節は、return 文による場合でも、try...catch からの_あらゆる_ exit の場合に機能します。try...catch が完了した直後ですが、呼び出し元のコードが制御を取得する前です。

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

f(); // cleanup!

…または、次のように throw がある場合

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

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

f(); // cleanup!

ここでクリーンアップを保証するのは finally です。f の最後にコードを配置するだけでは、これらの状況では実行されません。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHub issue またはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10 行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。