予期しないエラーからアプリを守る「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: エラーがあってもなくても最後に実行される場所(片付け・後処理に使う)
catch と finally は、どちらか片方だけを書くことも可能です。両方必須ではありません。
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/エンコードの失敗など |
EvalError | eval() の使い方が不正だったとき(ふだんはあまり使わない) |
この分類を知っておくと、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 で扱う
非同期処理を扱う標準的な書き方として、Promise や async/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は、今のフロントエンド開発で基本中の基本。
正直、「エラーハンドリングは後でいいや」と後回しにすると、あとで地獄になります…。
逆に最初から入れるクセをつけると、動かないときも落ち着いて状況を把握できて、自信を持って修正できるようになります。


