try/catch は「シートベルト」ではなく「セーフティネット」
JavaScript の行で例外が投げられると、そこで実行がピタッと止まり、エラーはコールスタックを遡っていきます。どこでもキャッチされなければ、プログラムはクラッシュ(Node の場合)するか、ブラウザのコンソールに真っ赤なエラーが並ぶことになります。この流れを途中で受け止めるのが try/catch です。「ここは失敗するかもしれない、そのときはこう対処する」と明示的に宣言する仕組みですね。
基本の書き方はこんな感じです。
JSON.parse は SyntaxError を投げます。処理はすぐに catch ブロックへ飛び、エラーは err にバインドされます。3 つ目の console.log はちゃんと実行される点に注目してください。クラッシュはしっかり封じ込められているわけです。
try ブロックが何も投げずに正常終了した場合、catch ブロックはまるごとスキップされます。あくまで失敗したときのための経路なんですね。
エラーオブジェクトの中身
投げられたものは、catch (...) のパラメータ名にそのままバインドされます。多くの場合それは Error のインスタンスで、役に立つフィールドが 3 つあります。
name はサブクラス名(TypeError、RangeError、SyntaxError など。詳しくは次のドキュメントで扱います)、message は人間が読むための説明、stack は完全なスタックトレースで、デバッグ時にはこれが何より頼りになります。
ひとつ注意点があります。JavaScript では Error オブジェクトに限らず、throw で何でも投げられてしまいます。古いコードだと throw "something broke" のように書かれていることもあります。自分で throw を書くときは、呼び出し側がスタックトレースを受け取れるように、必ず Error を投げるようにしましょう。
finally は必ず実行される
finally はオプションの3つ目のブロックで、エラーが投げられたかどうか、そして catch で処理されたかどうかに関係なく必ず実行されます。使いどころは後片付けです。ファイルを閉じたり、ロックを解放したり、ローディングスピナーを非表示にしたり、といった処理に向いています。
ローディング中のスピナーは、読み込みが成功してもエラーになっても非表示になります。finally がなければ、両方の分岐に同じ行を書く羽目になり、どちらかに書き忘れるのがオチです。
ちなみに finally は、try や catch のブロック内に return があっても実行されます。関数が値を返すのは finally が走った「後」です。たまに「え、そうなの?」と驚くこともありますが、たいていの場合はむしろ期待どおりの挙動でしょう。
catch は必須じゃない
実は catch は省略できます。try/finally だけの形も文法的に正しく、後片付けだけは確実にやりたいけどエラー自体はハンドリングせず、そのまま上に投げたいときに役立ちます:
内側の try/finally は、fn() が例外を投げた場合でもロックを解放します。しかもエラーを握りつぶさないので、呼び出し元にはちゃんとエラーが伝わります。エラーを黙って握りつぶす(「失敗したけど誰にも言わない」状態)のは、デバッグを地獄に変える最悪のアンチパターンのひとつです。
エラーの再スロー:一部だけ処理して残りは上に投げる
catch ブロックですべてのエラーを処理する必要はありません。中身を確認して、対応できるものだけ処理し、それ以外は再スローするというやり方が使えます。
instanceof によるチェックは定番のパターンです。つまり「自分が対処できるエラーだけを拾い、それ以外はそのまま呼び出し元へ投げ上げる」という考え方ですね。空の catch ブロックで全部握りつぶすのはコードスメルです。想定外の不具合が起きたときに、手がかりが一切残らなくなってしまいます。
async/await での try catch の使い方
async 関数の中では、await した Promise が reject されると例外としてスローされます。そのため、同期処理のエラーとまったく同じ感覚で try/catch で捕まえられます。
ひとつ注意点があります。Promise は必ず try ブロックの内側で await してください。await せずに Promise をそのまま返してしまうと、関数を抜けたあとで reject されることになり、catch には届きません。
async function bad() {
try {
return fetch("/broken"); // awaitなし — 呼び出し元がrejectionを受け取る
} catch (err) {
// 実行されない
}
}
基本ルール: async 関数の中では、try/catch でカバーしたい処理は必ず await すること。
try/catch のネスト
内側と外側でエラーの種類が違っていて、それぞれ別々に処理したいときは、try/catch をネストして書けます。
内側の catch は「データの形がおかしい」場合を安全なデフォルト値を返して処理し、外側の catch は「そもそも JSON ですらなかった」ケースをラップして再スローしています。各レイヤーにそれぞれ異なるリカバリ戦略があるなら、ネストさせても問題ありません。ただし、どちらのブロックも同じ処理をするだけなら、フラットにまとめましょう。
try/catch を使うべきでない場面
try/catch は、想定済みで回復可能な失敗に対処するための道具です。バグを隠すための手段ではありません。
- 関数全体を「念のため」で丸ごと包むのはやめましょう。エラーに対する具体的な対処方針がないなら、そのまま上位に投げさせたほうがいいです。スタックトレース付きの未捕捉エラーのほうが、こっそり握りつぶされるよりずっと役に立ちます。
- 制御フローの代わりに使わない。
tryブロックには実行コストがあり、ifによる単純なチェックに比べてコードも読みにくくなります。try { user.name } catch {}よりif (user)のほうが圧倒的に明快です。 - catch して log だけ出して無視する、もやめましょう。最低でも再スローするか、呼び出し側が検知できるようなセンチネル値を返すべきです。
判断基準はシンプルで、「このコードの利用者は、失敗したとき何をすればいいのか?」と自問することです。答えが出てこないなら、まだ catch する段階ではありません。
チートシート
try { ... } catch (err) { ... }— スローされたエラーを捕捉する。finally { ... }— 必ず実行される。クリーンアップ処理に使う。throw new Error("...")— スタックトレースが使えるよう、必ずErrorのサブクラスをスローする。catch内のthrow err;— 自分で処理しきれないときは再スローする。try内のawait— 非同期の reject をtry/catchで捕まえるには必須。
次は: エラーの種類について
TypeError、RangeError、SyntaxError など、JavaScript には組み込みのエラークラスが一通り揃っています。どれが何を意味するのかを押さえておくと、捕捉もレポートもぐっと的確になります。次のドキュメントではそのあたりを扱います。
よくある質問
JavaScriptのtry/catchはどう動くの?
失敗する可能性のあるコードを try { ... } の中に書きます。その中で何かが throw されると、実行はそのまま catch (err) { ... } に飛び、投げられた値が err に入ります。何も起きなければ catch はスキップ。後ろに finally { ... } を付けておけば成功・失敗どちらのケースでも必ず走るので、後始末の処理を書くのに便利です。
try/catchはどういう場面で使えばいい?
実行時に現実的に失敗しうる処理を囲むのが基本です。たとえば信頼できない入力を JSON.parse する、fetch のレスポンスを扱う、ファイルやネットワークI/Oなどですね。逆に、何でもかんでも包むのはNG。そこでエラーを受け止めても対処しようがないなら、そのまま上に投げさせるべきです。広すぎる try/catch はバグを隠すだけで、ハンドリングにはなりません。
try/catchで非同期のエラーもキャッチできる?
try の中で Promise を await したときだけキャッチできます。await を付けずに somePromise() を呼ぶだけだと catch には届かず、unhandled rejection になってしまいます。async/await を使っていれば同期コードと同じ感覚で書けますし、生の Promise を扱うときはチェーンの末尾に .catch() を付けるのが定番です。
catchしたエラーを再スローするには?
catch ブロックの中で throw err; と書くだけです(別のエラーでラップして投げ直すのもアリ)。「一部のエラーだけ自前で処理して、それ以外は上に任せたい」というときに使います。エラーの型やメッセージを見て、扱えるものだけ対処、それ以外は throw で呼び出し元に伝搬させる、というのがよくあるパターンです。