Q1try { console.log(1); } finally { console.log(2); } は何を表示しますか?
例外を受け止める — try / catch / finally
実行中に発生した例外を受け止める try と catch、必ず実行される finally を扱います。エラーの種類を表す name、呼び出し側へ投げ直す throw、種類ごとの分岐まで確かめます。
受け取った文字列を JSON.parse で戻す処理は、文字列が 1 文字でも崩れているとその行で止まり、下に書いた表示も保存も動きません。止まる原因を if ですべて事前に潰すこともできません。
この記事では、止まった処理を受け止める try / catch と、必ず実行される finally を扱います。
止まった処理を受け止める — try と catch
サーバーから受け取った設定の文字列を復元して、画面に反映したいとします。文字列の形が崩れていると JSON.parse はその行で 例外(処理を中断させるエラー)を発生させ、そこから下の行は 1 つも実行されません。画面には何も出ないまま止まります。
try(失敗するかもしれない処理を囲むブロック)の中で例外が起きると、実行は catch(例外を受け取るブロック)へ移ります。catch (error) の error は受け取った値に付ける名前で(err などに変えても動きます)、種類の名前を持つ name と説明文の message が入っています。
// 末尾に余分なカンマが残っている文字列
const configText = '{"theme":"dark","fontSize":14,}';
try {
const config = JSON.parse(configText);
console.log(config.theme); // この行は実行されない
} catch (error) {
// 例外が起きるとここへ移ってくる
console.log("設定を読み込めませんでした"); // 設定を読み込めませんでした
console.log(error.name); // SyntaxError
}
// catch を書いたので、続きの行は動く
console.log("既定の設定で表示します"); // 既定の設定で表示します
catch へ移ったあとは、例外が起きた行の続きには戻らないので、代わりの表示や既定値は catch の中に書きます。message の文言はブラウザによって変わるため、種類を見分けるときは name を読みます。
成功しても失敗しても実行する — finally
読み込み中の表示を消す、開いた接続を閉じるといった後片付けは、処理が成功しても失敗しても必要です。try と catch の両方に同じ行を書くと、片方だけ直したときに動きがずれます。
finally(try を抜けるときに必ず実行されるブロック)は catch のうしろに書きます。try が最後まで動いたときも、catch が例外を受け取ったときも、抜ける直前に必ず通ります。catch を省いて、try と finally だけを書くこともできます。
function loadReport(text) {
try {
const report = JSON.parse(text);
console.log(report.title);
} catch (error) {
console.log("読み込みに失敗しました");
} finally {
// try を抜けるときに必ず通る
console.log("読み込み中の表示を消しました");
}
}
// 成功したとき
loadReport('{"title":"9 月の売上"}'); // 9 月の売上 → 読み込み中の表示を消しました
// 失敗したとき
loadReport('{"title":}'); // 読み込みに失敗しました → 読み込み中の表示を消しました
finally は try か catch の処理が終わったあとに通ります。上のコードでも、成功したときはタイトルの後、失敗したときは失敗の案内の後に、片付けの表示が出ています。
try の中で宣言した変数は外から読めない
try の中で const や let で宣言した変数は、そのブロックの中でしか使えません。catch・finally やその先の行から読むと ReferenceError になります。あとで使う値は try の前に let で宣言し、try の中では代入だけを行います。
呼び出し側に知らせる — catch の中の throw
受付データを読む関数の中で例外を catch し、ログを出すだけにしたとします。関数はそのまま抜けるので、呼び出し側は失敗に気づかないまま戻り値を使い続け、原因とは離れた別の行で改めて処理が止まります。
throw(例外を発生させる文)に、catch が受け取った error をそのまま渡すと、同じ例外が呼び出し側へ伝わります。関数の中では記録を残し、どう対応するかの判断は呼び出し側に任せられます。
function readEntry(text) {
try {
return JSON.parse(text);
} catch (error) {
console.log(`受付データを読めません: ${error.name}`);
throw error; // 受け取った例外をそのまま投げ直す
}
}
try {
const entry = readEntry('{"name":"田中","seats":}');
console.log(entry.name); // この行は実行されない
} catch (error) {
console.log("受付を中止しました"); // 受付を中止しました
}
関数の中で受け止めた例外に呼び出し側でも対応が要るなら、catch の最後で throw error; と書いて投げ直します。throw より下の行は実行されないので、記録を残す行はその前に書きます。
空の catch は失敗を無かったことにする
catch (error) { } と中身を空にすると例外は消えたように見えますが、失敗した事実がどこにも残らず、原因を後から追えません。画面だけが空のまま、というのがいちばん起きやすい状態です。案内を出す・既定値を使う・投げ直すのいずれかを必ず書きます。
エラーの種類で分ける — instanceof と name
catch は種類を問わず、すべての例外を受け取ります。データの形が崩れていたのか、読もうとした値が入っていなかったのかで、利用者に出したい案内は変わります。
instanceof(左の値が右の種類から作られたかを判定する演算子)を使うと、error instanceof SyntaxError のように種類を判定できます。error.name にも同じ種類の名前が文字列で入っています。
const applicant = { name: "田中", contact: null };
try {
console.log(applicant.contact.email);
} catch (error) {
if (error instanceof SyntaxError) {
console.log("データの形式が不正です"); // ここには来ない
} else if (error instanceof TypeError) {
console.log("読もうとした値が入っていません"); // 読もうとした値が入っていません
} else {
console.log(`想定外の種類です: ${error.name}`);
}
console.log(error.name); // TypeError
}
catch (error) の 1 か所に届きますが、name が違います。種類を判定すれば、案内を分けられます。判定に当てはまらない例外は、最後の else でまとめて受け止めます。下の表は、よく出会う 4 種類と、それが起きる場面です。
| 種類 | 起きる場面 | 起きるコードの例 |
|---|---|---|
| SyntaxError | 文字列が JSON の形になっていない | JSON.parse("{,}") |
| TypeError | null や undefined のプロパティを読んだ | applicant.contact.email |
| RangeError | 桁数の指定など、許される範囲を超えた値を渡した | (1980).toFixed(200)(桁数は 100 まで) |
| ReferenceError | 宣言していない名前を読んだ | console.log(total) |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2関数の中の catch でログだけを出し、throw error; を書かなかったとき、呼び出し側が受け取る戻り値は?
Q3catch が受け取った error の種類を判定するときに使う演算子はどれですか?