Menu

JavaScriptのtry/catch入門|エラー処理の書き方

JavaScriptのtry/catch/finallyの基本から、errorオブジェクトの扱い方、再スロー、async/awaitでの使いどころまで実例ベースで解説します。

このページのコードはエディタで実行できます — 編集してすぐに結果を確認できます。

try/catch は「シートベルト」ではなく「セーフティネット」

JavaScript の行で例外が投げられると、そこで実行がピタッと止まり、エラーはコールスタックを遡っていきます。どこでもキャッチされなければ、プログラムはクラッシュ(Node の場合)するか、ブラウザのコンソールに真っ赤なエラーが並ぶことになります。この流れを途中で受け止めるのが try/catch です。「ここは失敗するかもしれない、そのときはこう対処する」と明示的に宣言する仕組みですね。

基本の書き方はこんな感じです。

JSON.parseSyntaxError を投げます。処理はすぐに catch ブロックへ飛び、エラーは err にバインドされます。3 つ目の console.log はちゃんと実行される点に注目してください。クラッシュはしっかり封じ込められているわけです。

try ブロックが何も投げずに正常終了した場合、catch ブロックはまるごとスキップされます。あくまで失敗したときのための経路なんですね。

エラーオブジェクトの中身

投げられたものは、catch (...) のパラメータ名にそのままバインドされます。多くの場合それは Error のインスタンスで、役に立つフィールドが 3 つあります。

name はサブクラス名(TypeErrorRangeErrorSyntaxError など。詳しくは次のドキュメントで扱います)、message は人間が読むための説明、stack は完全なスタックトレースで、デバッグ時にはこれが何より頼りになります。

ひとつ注意点があります。JavaScript では Error オブジェクトに限らず、throw で何でも投げられてしまいます。古いコードだと throw "something broke" のように書かれていることもあります。自分で throw を書くときは、呼び出し側がスタックトレースを受け取れるように、必ず Error を投げるようにしましょう。

finally は必ず実行される

finally はオプションの3つ目のブロックで、エラーが投げられたかどうか、そして catch で処理されたかどうかに関係なく必ず実行されます。使いどころは後片付けです。ファイルを閉じたり、ロックを解放したり、ローディングスピナーを非表示にしたり、といった処理に向いています。

ローディング中のスピナーは、読み込みが成功してもエラーになっても非表示になります。finally がなければ、両方の分岐に同じ行を書く羽目になり、どちらかに書き忘れるのがオチです。

ちなみに finally は、trycatch のブロック内に 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 で捕まえるには必須。

次は: エラーの種類について

TypeErrorRangeErrorSyntaxError など、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 で呼び出し元に伝搬させる、というのがよくあるパターンです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める