JavaScriptのエラーハンドリング入門

7 min 39 views
errorhandling

予期しないエラーからアプリを守る「try…catch」と非同期の考え方

WebサイトやWebアプリは、いろんなレベルのユーザーが使います。

想定通りの入力をしてくれる人もいれば、思いもよらない操作をする人もいますよね。

たとえば

  • 数字だけ入れてほしいところに文字を入れる
  • 途中でネットが切れる
  • 必須入力を空のまま送信しちゃう

こういう“想定外”が起きたとき、もしそのままエラーになってページが止まってしまったら、ユーザーは不安になりますし、信頼も落ちます。

そこで必要になるのが エラーハンドリング(例外処理) です。
これは、エラーが起きたときに「どう振る舞うか」を事前に定義しておく仕組みです。

開発者にとってはデバッグもしやすくなり、ユーザーにとっては「壊れて見える瞬間」を減らすことができます。

エラーハンドリングとは?

エラーハンドリング(例外処理)とは、

  • 実行中に問題が起こったときに
  • そのままアプリをクラッシュさせず
  • 代わりの処理に切り替える

という考え方です。

よくある「起こりがちな問題」はこのあたり

  • 入力値が想定外(空文字や文字列なのに数値が必要な場面 など)
  • サーバーからデータが返ってこない(通信エラー)
  • 必要な変数や関数が見つからない
  • 計算できない状態(0で割ろうとした とか)

こういうとき、ちゃんと受け止めてあげるのがエラーハンドリングの役割です。

JavaScriptでは主に try…catch 構文 を使ってこれを実現します。

まずはエラーを見てみる

次のような関数を考えてみましょう。

税込価格を計算したいイメージです。

function getTaxValue(unitPrice) {
  let tax = unitPrice * tax_rate;
  return tax;
}

getTaxValue(1000);

ここには問題があります。tax_rate が定義されていませんよね。
この状態で実行すると、コンソールにこういったエラーが表示されます。

Uncaught ReferenceError: tax_rate is not defined

これはtax_rate なんて変数ないよ」というエラーです。
このままだと処理が止まってしまいます。

これを安全に扱うのが try…catch です。

try…catch 構文でエラーをキャッチする

try...catch は「危なそうな処理」を try の中に入れ、もしエラーになったら catch の中で代わりの処理をする、という仕組みです。

さきほどの関数を修正してみます。

function getTaxValue(unitPrice) {
  try {
    const tax = unitPrice * tax_rate; // ← ここでコケる可能性がある
    return tax;
  } catch (err) {
    console.error(`${err.message} が発生しました。`);
    return; // 失敗したので何も返さない(undefinedを返すイメージ)
  }
}

getTaxValue(1000);

ここでの動きはこうです

  • try ブロック内の処理を実行する
  • 万が一エラーが出たら、catch ブロックに飛ぶ
  • catch の中ではアプリ全体を止めずに、エラーメッセージを整えて出力したり、代わりの値を返したりできる

このおかげで「画面が真っ白になる」「止まって何もできない」を防げます。

try…catch の基本構造

ポイントだけおさえましょう。

try {
  // エラーが出るかもしれない処理
} catch (error) {
  // エラーが起きたときの処理
} finally {
  // 最後に必ず実行したい処理(後述)
}
  • try: 危ない処理を書く場所
  • catch(error): エラーが起きたときに呼ばれる場所。error にはエラー情報が入る
  • finally: エラーがあってもなくても最後に実行される場所(片付け・後処理に使う)

catchfinally は、どちらか片方だけを書くことも可能です。両方必須ではありません。

finally ブロックって何に使うの?

finally は「エラーの有無に関わらず、最後に必ずやりたいこと」を書く場所です。

たとえば

  • ローディング表示を消す
  • 開いていた接続を閉じる
  • 一時的に書き換えた状態を元に戻す

イメージコード

function runProcess() {
  showLoadingSpinner(); // ぐるぐるUIを出す

  try {
    riskyTask(); // ここで落ちる可能性あり
  } catch (err) {
    console.error("処理に失敗しました:", err.message);
  } finally {
    hideLoadingSpinner(); // ぐるぐるUIは最終的に必ず消す
  }
}

ユーザー視点でも「いつまでもぐるぐるしている」状況を避けられるので、UX的にも大事です。

JavaScriptのエラーには種類がある

JavaScriptの実行中に発生するエラーは「Errorオブジェクト」として扱われます。

代表的なものを整理しておきます。

エラー名どういうときに起きる?
Error一般的なエラー全般(汎用)
ReferenceError存在しない変数・関数を使ったとき
SyntaxError文法(構文)がそもそも間違っているとき
TypeError型が合わないとき
(想定は数値なのに文字列だった…など)
RangeError許容範囲を超えた値が渡されたとき
URIError不正なURI/エンコードの失敗など
EvalErroreval() の使い方が不正だったとき
(ふだんはあまり使わない)

この分類を知っておくと、catch の中で「どういう種類の失敗なのか?」を人間向けに説明できます。

たとえば「この入力欄は数字で入力してください」みたいなユーザーメッセージに変換したいときにも役立ちます。

自分でエラーを投げる(throw)

ここからが実用的な話です。

JavaScriptは「変な値でもとりあえず動こうとする」ことがあるので、逆に気づきにくいバグが生まれます。
そこで、条件に合わないときは 自分からエラーを投げてしまう というやり方がよく使われます。

そのときに使うのが throw です。

try {
  // 自前のエラーを投げる
  throw new Error("無効な入力が渡されました");
} catch (err) {
  console.error("エラーを受け取りました:", err.message);
}

実行結果イメージ

エラーを受け取りました: 無効な入力が渡されました

これで「おかしな状態なのに静かに進んでしまう」というのを防げます。

 具体的な使いどころ(例:フォームのバリデーション)

たとえば「0以上の数しか受けつけたくない入力欄」があるとします。

function validatePositiveNumber(value) {
  if (value < 0) {
    // ここでわざと止める
    throw new Error("0以上の数値を入力してください");
  }
}

function handleSubmit() {
  try {
    const userValue = Number(document.getElementById("age").value);
    validatePositiveNumber(userValue);
    console.log("OKとして送信します");
  } catch (err) {
    // 入力エラー用の表示など
    alert(err.message);
  }
}

こうしておけば、フォーム送信前に「これは不正な入力です」と検知してユーザーに返せます。
「何も起きず送信できない」よりも、ちゃんと伝えてあげた方が使いやすいですよね。

非同期処理とエラーハンドリングは少しクセがある

ここがつまずきポイントです。
JavaScriptは「非同期」の処理(すぐに終わらない処理)をよく使います。

たとえば

  • setTimeout(...)
  • サーバーからデータを取ってくる fetch(...)
  • ユーザーの入力を待つイベントハンドラ
  • 画像やファイルの読み込み

問題は、非同期で発生したエラーは、外側の try...catch ではそのまま拾えないことがある という点です。

 良くある失敗例

function boomLater() {
  throw new Error("あとで爆発したエラー");
}

try {
  setTimeout(boomLater, 500);
} catch (err) {
  // ここには来ない!
  console.log("キャッチ:", err.message);
}

console.log("この行はすぐ実行される");

実際の流れはこうなります

  • setTimeout 自体はすぐにスケジュールされて終わる
  • try ブロックはもう抜ける
  • 500ms後に boomLater() が呼ばれる
  • そのときに投げられたエラーは、もはや try...catch の外側で発生しているので、catch に届かない

つまり「非同期の中で起きたエラーは、非同期の中で扱う」必要があります。

非同期の中でエラーを扱うには

基本パターンは2つあります。

 パターン1:コールバックの中で try…catch する

function boomLaterSafely() {
  try {
    throw new Error("タイマー内のエラーです");
  } catch (err) {
    console.log("拾えました:", err.message);
  }
}

setTimeout(boomLaterSafely, 500);
console.log("この行も普通に実行される");

ここでは、エラーを投げる関数自体が try...catch を持っているので、タイマーの中で起きた例外をちゃんと処理できます。

このイメージを持っておくのがすごく大事です。
「外からまとめて拾おう」と思っても、非同期はそう簡単には拾えない、ということです。

 パターン2:Promise / async-await で扱う

非同期処理を扱う標準的な書き方として、Promiseasync/await(前の章で学んだもの)が使えます。
これらには、エラーを「成功(resolve)か失敗(reject)か」という形で返す仕組みがあります。

Promiseの基本形(おさらい)

function doAsyncTask() {
  return new Promise((resolve, reject) => {
    // ここで非同期の処理を書く
    // うまくいったら resolve(...)
    // エラーなら reject(...)

    // 例: 成功パターン
    // resolve("成功しました");

    // 例: エラーパターン
    reject(new Error("サーバーからエラーが返ってきました"));
  });
}

doAsyncTask()
  .then((result) => {
    console.log("成功時:", result);
  })
  .catch((err) => {
    console.error("失敗時:", err.message);
  });

ここでは catch(...) が非同期のエラー受け取り役になっています。
つまり「エラーは reject から catch に流れてくる」イメージです。

async / await ならもっと読みやすい

async 関数内なら、await で Promise の結果を待つことができます。
そのときはふつうの try...catch で書けるのが嬉しいポイントです。

function fetchUserData() {
  return new Promise((resolve, reject) => {
    // 通信に成功したと仮定
    // resolve({ name: "Yamada", age: 28 });

    // 通信エラーを再現したいならこちら
    reject(new Error("APIからデータを取得できませんでした"));
  });
}

// async関数として定義
async function loadProfile() {
  try {
    const data = await fetchUserData(); // 結果が返るまで待つ
    console.log("取得できたユーザー情報:", data);
  } catch (err) {
    console.error("プロフィールの取得に失敗:", err.message);
  } finally {
    console.log("プロフィール読み込み処理を終了します");
  }
}

loadProfile();

ここでのメリットは、「非同期だけど、見た目は同期っぽく書ける」 ということ。
try...catch が素直に働いてくれるので、読みやすく・保守しやすいコードになります。

まとめ:エラーハンドリングを“最初から”入れるクセをつける

  • エラーハンドリングはアプリを守る安全装置。ユーザー体験と開発効率の両方に関わります。
  • try...catch は、エラーが起きそうな処理を囲って安全に回復させる仕組み。
  • finally は後片付け用。通信中ローディングを消すなどの「必ずやりたいこと」に便利。
  • throw new Error(...) を自分で投げれば「こんな入力はダメ!」などを明確に扱える。
  • 非同期処理は別物。setTimeout などのコールバックの外側では catch できないので、コールバック内やPromise/async-await側で処理する。
  • Promise.then(...).catch(...)async/await + try...catch は、今のフロントエンド開発で基本中の基本。

正直、「エラーハンドリングは後でいいや」と後回しにすると、あとで地獄になります…。
逆に最初から入れるクセをつけると、動かないときも落ち着いて状況を把握できて、自信を持って修正できるようになります。

created by Rinker
¥5,060 (2025/11/03 12:19:43時点 楽天市場調べ-詳細)
関連記事