順番に読み進めながら学べます

例外を受け止める — 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("既定の設定で表示します");         // 既定の設定で表示します
try で囲まないときと囲んだとき
try で囲まずにparse するその行で例外が起きる移る先がどこにも無い以降の行は実行されないtry で囲んでparse するその行で例外が起きるcatch (error)へ移るtry の外の行が実行される
例外が起きた行のすぐ下は、どちらの書き方でも実行されません。try で囲むと制御が catch へ移り、try の外の行まで進みます

catch へ移ったあとは、例外が起きた行の続きには戻らないので、代わりの表示や既定値は catch の中に書きます。message の文言はブラウザによって変わるため、種類を見分けるときは name を読みます。

保存しておいた下書きからタイトルを取り出します。savedDraft と brokenDraft は宣言済みです。

① restoreTitle で文字列を復元し、タイトルを返してください。

② 失敗したら種類の名前を表示し、「無題の下書き」を返してください。

③ 2 つの文字列で呼び、「タイトル: 9 月の週報」の形で表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

成功しても失敗しても実行する — finally

読み込み中の表示を消す、開いた接続を閉じるといった後片付けは、処理が成功しても失敗しても必要です。trycatch の両方に同じ行を書くと、片方だけ直したときに動きがずれます。

finally(try を抜けるときに必ず実行されるブロック)は catch のうしろに書きます。try が最後まで動いたときも、catch が例外を受け取ったときも、抜ける直前に必ず通ります。catch を省いて、tryfinally だけを書くこともできます。

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":}');             // 読み込みに失敗しました → 読み込み中の表示を消しました
2 つの経路が同じ場所を通る
整った文字列を渡すtry の中が最後まで動くcatch は飛ばされるfinally が動く崩れた文字列を渡すtry の途中で例外が起きるcatch が受け取るfinally が動く
上と下では途中で通る場所が違うのに、右端は同じです。finally は成功しても失敗しても実行されます

finallytrycatch の処理が終わったあとに通ります。上のコードでも、成功したときはタイトルの後、失敗したときは失敗の案内の後に、片付けの表示が出ています。

try の中で宣言した変数は外から読めない

try の中で constlet で宣言した変数は、そのブロックの中でしか使えません。catchfinally やその先の行から読むと ReferenceError になります。あとで使う値は try の前に let で宣言し、try の中では代入だけを行います。

会員データを 1 行ずつ取り込み、進み具合を表示します。rows と processed は宣言済みで、2 行目は形が崩れています。

① 1 行ずつ復元し、「佐藤: 年額プラン」の形で表示してください。

② 復元に失敗したときは「取り込めない行がありました」と表示してください。

③ 成功しても失敗しても processed を 1 増やし、「1 / 3 件目を処理しました」の形で表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

呼び出し側に知らせる — 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 あり同じ SyntaxErrorが外へ出る外側の catch が受け取る「受付を中止しました」catch にthrow error なし戻り値はundefinedentry.name でTypeError原因とは別の例外が届く
下の段は関数の中で例外が終わり、undefined を読んだ行で改めて止まります。投げ直すと、外側に原因の SyntaxError が届きます

関数の中で受け止めた例外に呼び出し側でも対応が要るなら、catch の最後で throw error; と書いて投げ直します。throw より下の行は実行されないので、記録を残す行はその前に書きます。

空の catch は失敗を無かったことにする

catch (error) { } と中身を空にすると例外は消えたように見えますが、失敗した事実がどこにも残らず、原因を後から追えません。画面だけが空のまま、というのがいちばん起きやすい状態です。案内を出す・既定値を使う・投げ直すのいずれかを必ず書きます。

保存したレイアウトを読み込みます。savedLayout、layout、errorLog は宣言済みです。

① loadLayout で文字列を復元して返してください。

② 失敗したら errorLog に種類の名前を追加し、投げ直してください。

③ 結果で layout を置き換え、失敗したら「既定のレイアウトで表示します」と表示してください。

④ 列数と記録を「列数: 2」「記録: SyntaxError」の形で表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

エラーの種類で分ける — 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 に届く 3 つの例外
try の中で起きた例外崩れた JSON をparse するcontact が nullのまま読むtoFixed に 200桁を指定するname はSyntaxErrorname はTypeErrorname はRangeError形式が不正と案内する値が入っていないと案内想定外として記録する
どれも catch (error) の 1 か所に届きますが、name が違います。種類を判定すれば、案内を分けられます

判定に当てはまらない例外は、最後の else でまとめて受け止めます。下の表は、よく出会う 4 種類と、それが起きる場面です。

種類起きる場面起きるコードの例
SyntaxError文字列が JSON の形になっていないJSON.parse("{,}")
TypeErrornull や undefined のプロパティを読んだapplicant.contact.email
RangeError桁数の指定など、許される範囲を超えた値を渡した(1980).toFixed(200)(桁数は 100 まで)
ReferenceError宣言していない名前を読んだconsole.log(total)

気温センサーから届いた文字列を取り込みます。payloads は宣言済みで、3 件のうち 2 件に問題があります。

① 1 件ずつ復元し、「T-1: 21.5 度」の形で表示してください。気温は小数第 1 位にそろえます。

② 形式が崩れていたときは「受信データの形式が不正です」と表示してください。

③ 気温が入っていないときは「気温が入っていません」と表示してください。

④ どちらでもないときは「想定外です: 」に続けて種類の名前を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1try { console.log(1); } finally { console.log(2); } は何を表示しますか?

Q2関数の中の catch でログだけを出し、throw error; を書かなかったとき、呼び出し側が受け取る戻り値は?

Q3catch が受け取った error の種類を判定するときに使う演算子はどれですか?