Menu
日本語

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

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

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

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

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

index.js
Output
Click Run to see the output here.

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

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

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

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

index.js
Output
Click Run to see the output here.

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

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

index.js
Output
Click Run to see the output here.

finally は必ず実行される

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

index.js
Output
Click Run to see the output here.

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

ちなみに finally は、trycatch のブロック内に return があっても実行されます。関数が値を返すのは finally が走った「後」です。たまに「え、そうなの?」と驚くこともありますが、たいていの場合はむしろ期待どおりの挙動でしょう。

catch は必須じゃない

実は catch は省略できます。try/finally だけの形も文法的に正しく、後片付けだけは確実にやりたいけどエラー自体はハンドリングせず、そのまま上に投げたいときに役立ちます:

index.js
Output
Click Run to see the output here.

内側の try/finally は、fn() が例外を投げた場合でもロックを解放します。しかもエラーを握りつぶさないので、呼び出し元にはちゃんとエラーが伝わります。エラーを黙って握りつぶす(「失敗したけど誰にも言わない」状態)のは、デバッグを地獄に変える最悪のアンチパターンのひとつです。

エラーの再スロー:一部だけ処理して残りは上に投げる

catch ブロックですべてのエラーを処理する必要はありません。中身を確認して、対応できるものだけ処理し、それ以外は再スローするというやり方が使えます。

index.js
Output
Click Run to see the output here.

instanceof によるチェックは定番のパターンです。つまり「自分が対処できるエラーだけを拾い、それ以外はそのまま呼び出し元へ投げ上げる」という考え方ですね。空の catch ブロックで全部握りつぶすのはコードスメルです。想定外の不具合が起きたときに、手がかりが一切残らなくなってしまいます。

async/await での try catch の使い方

async 関数の中では、await した Promise が reject されると例外としてスローされます。そのため、同期処理のエラーとまったく同じ感覚で try/catch で捕まえられます。

index.js
Output
Click Run to see the output here.

ひとつ注意点があります。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 をネストして書けます。

index.js
Output
Click Run to see the output here.

内側の 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でコードを学ぼう

始める