エラーハンドリング — try/catch/finally・カスタムエラー・非同期のエラー
プログラムは思いどおりに動かないことがあります。APIが落ちている、ユーザーが想定外の値を入力した、ファイルが存在しない——こうした「失敗」をどう受け止め、どう立て直すかを決めるのがエラーハンドリングです。
前章までで throw で例外を「投げる」側は見ました。この章は、投げられた例外を受け止めて処理を続ける側、つまり try...catch...finally と、自分でエラーの種類を設計するカスタムエラークラス、そして非同期処理(async/await・Promise)でのエラーの捕まえ方を整理します。
学習者try...catch は書けるけど、catch (e) の e に何が入ってるのか、finally をいつ使うのか、よく分かってない…。await のエラーが捕まえられなくてハマったこともある。
その「なんとなく」を一つずつ潰していきましょう。throw 自体の基礎(throw は文、文字列を投げない、など)は制御構文の章で扱っているので、ここでは捕まえる側に集中します。
try...catch の基本 — エラーが起きても処理を止めない
JavaScriptは、エラー(例外)が発生するとその場で処理を中断し、呼び出し元へさかのぼっていきます。最後まで誰も受け止めなければ、プログラム全体が止まります。try...catch は、この「止まる」を自分のコードで受け止めるための仕組みです。
try {
const data = JSON.parse('壊れたJSON'); // ここで例外が発生
console.log(data); // 実行されない
} catch (error) {
console.log('パースに失敗しました'); // ここが実行される
}
console.log('処理は続行する'); // catch で受け止めたので、ここも実行されるtry ブロックの中でエラーが起きると、JavaScriptは残りの try を飛ばして即座に catch ブロックへジャンプします。受け止めたあとは、try...catch の外側の処理はそのまま続きます。
先生ポイントは「try の中でエラーが起きた瞬間に、残りの行は飛ばして catch に移る」こと。try を最後まで実行してから catch に行くわけじゃないんだ。
実行の流れを図にすると次のようになります。finally(後述)まで含めると、分岐があってもゴールは1か所に集まります。
catch が受け取る error オブジェクト — name・message・stack
catch (error) の error には、throw された値が入ります。慣習に従って Error オブジェクトが投げられていれば、デバッグに使える情報がプロパティとして詰まっています。
| プロパティ | 説明 |
|---|---|
name | エラーの種類名。"Error"・"TypeError" など。エラーの分類に使う |
message | コンストラクタに渡した説明文。new Error('xxx') の 'xxx' がここに入る |
stack | エラー発生箇所までの呼び出し履歴(スタックトレース)。デバッグ用。標準化はされていないがほぼ全環境で使える |
cause | 原因となった元のエラー(ES2022で追加。後述) |
try {
null.toUpperCase(); // null にメソッドはない → TypeError
} catch (error) {
console.log(error.name); // "TypeError"
console.log(error.message); // "Cannot read properties of null (reading 'toUpperCase')"
console.log(error.stack); // 呼び出し履歴(複数行の文字列)
}catch の引数は省略できる
エラーの中身を使わず「失敗したかどうか」だけ分かればよい場合、catch の引数((error) の部分)は省略できます(ES2019、Optional catch binding)。
function isValidJson(text) {
try {
JSON.parse(text);
return true;
} catch {
// error を使わないので括弧ごと省略できる
return false;
}
}
学習者catch (e) {} って書いてたけど、使わないなら catch {} でいいんだ。
組み込みのエラー型 — TypeError・RangeError…
JavaScriptには、状況に応じた組み込みのエラー型があります。すべて Error を継承していて、error.name で種類を見分けられます。自分でエラーを投げるときも、内容に合った型を選ぶと、受け取る側が分類しやすくなります。
| 型 | どんなときに発生するか |
|---|---|
TypeError | 値が期待した型でない。null のプロパティ参照、関数でないものの呼び出しなど(最頻出) |
RangeError | 値が許容範囲外。配列長に負数を入れる、再帰が深すぎるなど |
ReferenceError | 存在しない変数を参照した |
SyntaxError | 構文エラー。JSON.parse の失敗もこれ |
URIError | decodeURIComponent などに不正な文字列を渡した |
AggregateError | 複数のエラーをまとめたもの。Promise.any が全部失敗したときなど |
// 値の型がおかしい → TypeError
function double(n) {
if (typeof n !== 'number') {
throw new TypeError(`数値が必要です: ${typeof n}`);
}
return n * 2;
}
// 範囲がおかしい → RangeError
function setVolume(v) {
if (v < 0 || v > 100) {
throw new RangeError(`音量は0〜100です: ${v}`);
}
}finally — 成否にかかわらず必ず実行する
finally ブロックは、try が成功しても catch でエラーを受け止めても、どちらの場合も必ず最後に実行されます。「成否に関係なく後始末したい処理」を書く場所です。
代表例はリソースの解放です。ファイルを閉じる、ローディング表示を消す、接続を切る——成功しても失敗しても確実にやりたい処理を finally に置きます。
function loadData() {
showSpinner(); // ローディング開始
try {
const data = fetchSync();
render(data);
} catch (error) {
showError(error.message);
} finally {
hideSpinner(); // 成功しても失敗しても、必ずスピナーを消す
}
}
先生try と catch の両方に hideSpinner() を書くと、消し忘れや重複の温床になる。「どっちのルートでも通る後始末」は finally に一本化するのが定石だよ。
throw と再スロー — 握りつぶさず、必要なら投げ直す
catch で受け止めたからといって、何ごともなかったことにして良いとは限りません。自分で対処できないエラーは、より上の呼び出し元に判断を委ねるべきです。そのために catch の中でもう一度 throw するのが「再スロー」です。
function loadConfig(path) {
try {
return readFile(path);
} catch (error) {
// ここでログだけ残して、判断は呼び出し元に委ねる
console.error(`設定ファイルの読み込みに失敗: ${path}`, error);
throw error; // 再スロー:握りつぶさず上へ伝える
}
}
最もやってはいけないのが、catch で受け止めておきながら何もしない「握りつぶし」です。エラーが起きても画面上は正常に見えるため、原因不明のバグとして後から苦しむことになります。
// ❌ アンチパターン:エラーを握りつぶす
try {
saveToServer(data);
} catch {
// 何もしない → 保存に失敗しても誰も気づけない
}
学習者とりあえず try...catch で囲んでおけば落ちないから安心、って思ってた…。
「落ちない」のと「正しく動く」のは別物です。catch に入ったら、最低でもログを残す、対処できないなら再スローする、ユーザーにエラーを伝える——このいずれかは必ず行いましょう。
カスタムエラークラス — Error を継承して種類を作る
アプリが大きくなると、「これは入力ミス」「これは権限不足」「これはネットワーク障害」のように、エラーを種類で区別して扱い分けたくなります。組み込みの Error を extends して、独自のエラー型を定義できます。
class ValidationError extends Error {
constructor(message, field) {
super(message); // 親 Error の初期化(message をセット)
this.name = 'ValidationError'; // name は自分で設定する
this.field = field; // 独自の情報を持たせられる
}
}
throw new ValidationError('メールアドレスの形式が不正です', 'email');ポイントは2つです。super(message) で親の Error を初期化して message をセットすること、そして this.name を自分で設定すること(設定しないと name が "Error" のままになります)。field のように、エラー固有の情報を好きなだけ持たせられるのがカスタムエラーの強みです。
受け取る側は instanceof で種類を判定し、種類ごとに対応を変えられます。
try {
validateForm(input);
} catch (error) {
if (error instanceof ValidationError) {
// 入力ミス → その項目にエラー表示
highlightField(error.field, error.message);
} else {
// それ以外 → 想定外なので上へ投げ直す
throw error;
}
}
先生error.message の文字列で if (message.includes('...')) と判定するのは脆い。型(クラス)で分岐できるようにしておくと、メッセージを変えても壊れないんだ。
cause で「元のエラー」を保持する
低レベルのエラー(例:ネットワークエラー)を、より分かりやすいエラーに包み直して投げることがあります。このとき元のエラーを捨ててしまうと原因が追えなくなるので、Error の第2引数 options.cause(ES2022)に元のエラーを渡して保持します。
構文: new Error(message, options?)
| 引数 | 説明 |
|---|---|
message | エラーの説明文(error.message に入る) |
options(省略可) | { cause: 元のエラー } を渡すと、error.cause から原因をたどれる |
async function loadUser(id) {
try {
return await fetchUser(id);
} catch (error) {
// 元のエラーを cause に残しつつ、文脈を足して投げ直す
throw new Error(`ユーザー(${id})の取得に失敗しました`, { cause: error });
}
}非同期処理のエラーハンドリング
ここからが多くの人がつまずく本題です。非同期処理では、エラーが少し遅れてやってきます。捕まえ方は「async/await か Promise か」で書き方が変わります。
async/await — 同期と同じく try...catch で囲む
await した処理が失敗(reject)すると、その行で例外が投げられます。つまり同期コードと同じ感覚で try...catch で囲めます。これが async/await の最大のメリットです。
async function showUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
// fetch は404でも reject しない → 自分で throw する
throw new Error(`HTTP ${res.status}`);
}
const user = await res.json();
render(user);
} catch (error) {
showError(`読み込みに失敗しました: ${error.message}`);
}
}Promise — .catch() でつなぐ
.then() チェーンで書く場合は、末尾に .catch() を付けてエラーを受け止めます。チェーンのどこで失敗しても、まとめて .catch() に流れてきます。
fetch('/api/users/1')
.then((res) => res.json())
.then((user) => render(user))
.catch((error) => showError(error.message)); // どこで失敗してもここに来る
学習者async/await と .then().catch()、どっちで書けばいいの?
新しく書くなら、同期コードと同じ見た目で読める async/await + try...catch が基本です。.catch() は、短い単発の処理や、既存の Promise チェーンに合わせるときに使います。
複数の非同期処理 — Promise.all / allSettled / any
複数の Promise をまとめて扱うとき、1つ失敗したらどうするかで使うメソッドが変わります。
| メソッド | 1つ失敗したときの挙動 | 戻り値 |
|---|---|---|
Promise.all(iterable) | 即座に reject。1つでもコケると全体が失敗扱い | 全成功なら結果の配列 |
Promise.allSettled(iterable) | reject しない。全部の結果を待つ | 各要素が { status, value/reason } の配列 |
Promise.any(iterable) | 1つでも成功すれば成功。全部失敗で AggregateError | 最初に成功した値 |
// all:1つでも失敗したら catch に飛ぶ(成功した分の結果も受け取れない)
try {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
} catch (error) {
console.error('どれかが失敗しました', error);
}
// allSettled:失敗しても止めず、成功分だけ拾いたいとき
const results = await Promise.allSettled([fetchA(), fetchB()]);
const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
先生「全部そろわないと意味がない」なら all、「コケたものは飛ばして、取れたものだけ使いたい」なら allSettled。この使い分けは実務で本当によく出てくるよ。
よくあるハマりどころ

1. await を付け忘れると try...catch で捕まえられない
これが非同期エラー最大の落とし穴です。await を付け忘れた Promise の失敗は、try ブロックを素通りして外に漏れます。try を抜けた後で reject するため、catch には入りません。
// ❌ await がない → reject が catch に捕まらない
async function bad() {
try {
saveToServer(data); // Promise を投げっぱなし(await なし)
} catch (error) {
// ここには来ない!未処理のまま外に漏れる
}
}
// ⭕ await を付ければ、失敗はその行で例外になり catch に入る
async function good() {
try {
await saveToServer(data);
} catch (error) {
showError(error.message);
}
}
学習者catch が動かないからエラーハンドリングが壊れてると思ってたけど、原因は await の付け忘れだったのか…!
2. 同期の try...catch では非同期エラーを捕まえられない
setTimeout のコールバックなど、後から別の文脈で実行される処理の中のエラーは、それを仕掛けた側の try...catch では捕まりません。コールバックが動くころには、もう try ブロックを抜けているからです。
// ❌ setTimeout の中のエラーは、この try では捕まえられない
try {
setTimeout(() => {
throw new Error('遅れて発生'); // どこにも捕まらず未処理に
}, 1000);
} catch (error) {
// ここには来ない
}捕まえたいなら、try...catch はコールバックの中に書きます。
3. catch した値が必ずしも Error とは限らない
throw は技術的にどんな値でも投げられるため、ライブラリやコードによっては文字列やオブジェクトが投げられることがあります。error.message を無条件にアクセスすると、それ自体でエラーになることも。instanceof Error で確認してから扱うと安全です。
try {
doSomething();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
showError(message);
}ちゃんと使うためのポイント
try...catchは エラーで処理を止めずに受け止めるための仕組み。try内でエラーが起きた瞬間にcatchへジャンプするcatchのerrorにはname・message・stackが入る。ログにはerror全体を渡してスタックトレースを残すfinallyは成否にかかわらず必ず実行される。後始末(リソース解放)を一本化する場所。return/throwは書かない- 握りつぶし禁止。
catchに入ったら最低でもログを残し、対処できないなら再スローする - エラーを種類で扱い分けたいなら
Errorを継承したカスタムエラークラスを作り、instanceofで分岐する。元の原因はcauseで残す async/awaitのエラーはtry...catch、Promise チェーンは.catch()。awaitの付け忘れはcatchをすり抜けるので最注意
次の章では、コードをファイル単位で分割して再利用する モジュール(import / export) を扱います。エラーを投げるユーティリティやカスタムエラークラスを別ファイルに切り出して使い回す、という形でも自然につながっていきます。
