はじめに#
プログラムを書いていると、必ずといっていいほどエラーに遭遇します。ネットワーク接続の失敗、ユーザーの不正な入力、存在しないファイルへのアクセスなど、エラーの原因は多岐にわたります。
JavaScriptにおけるエラーハンドリングとは、こうしたエラーを適切に検知し、プログラムがクラッシュすることなく、ユーザーにわかりやすいフィードバックを返すための技術です。
本記事では、以下の内容を初心者向けにわかりやすく解説します。
- エラーとは何か、なぜエラーハンドリングが重要なのか
try-catch-finally文の基本構文と使い方
- JavaScriptのエラーオブジェクトの種類
throw文による意図的なエラーの発生
- 非同期処理(Promise・async/await)でのエラーハンドリング
- 実践的なエラーハンドリングのパターン
エラーハンドリングとは#
なぜエラーハンドリングが重要なのか#
エラーハンドリングを行わないと、エラーが発生した時点でプログラムの実行が停止してしまいます。これはユーザー体験を大きく損ない、場合によってはデータの損失にもつながります。
1
2
3
4
|
// エラーハンドリングなし - プログラムが停止
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
console.log("この行は実行されない");
|
適切なエラーハンドリングを行うことで、以下のメリットが得られます。
| メリット |
説明 |
| アプリの安定性向上 |
エラーが発生してもプログラムが継続動作 |
| ユーザー体験の改善 |
わかりやすいエラーメッセージを表示可能 |
| デバッグの効率化 |
エラーの発生箇所と原因を特定しやすい |
| セキュリティの向上 |
内部エラー情報の漏洩を防止 |
エラーの種類#
JavaScriptで発生するエラーは、大きく3つに分類されます。
| 種類 |
説明 |
例 |
| 構文エラー(Syntax Error) |
コードの文法が間違っている |
括弧の閉じ忘れ |
| 実行時エラー(Runtime Error) |
実行中に発生するエラー |
未定義の変数へのアクセス |
| 論理エラー(Logic Error) |
コードは動くが結果が意図と異なる |
計算式の間違い |
エラーハンドリングで対処できるのは、主に実行時エラーです。構文エラーはコードの実行前に検出され、論理エラーはテストやデバッグで発見する必要があります。
try-catch文の基本#
基本構文#
try-catch文は、エラーが発生する可能性のあるコードを安全に実行するための構文です。
1
2
3
4
5
|
try {
// エラーが発生する可能性のあるコード
} catch (error) {
// エラーが発生したときの処理
}
|
tryブロック内でエラーが発生すると、即座にcatchブロックに処理が移ります。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
try {
const user = null;
console.log(user.name); // ここでエラー発生
console.log("この行は実行されない");
} catch (error) {
console.log("エラーをキャッチしました:", error.message);
}
console.log("プログラムは継続します");
// 出力:
// エラーをキャッチしました: Cannot read properties of null (reading 'name')
// プログラムは継続します
|
エラーオブジェクトのプロパティ#
catchブロックで受け取るエラーオブジェクトには、エラーに関する情報が含まれています。
| プロパティ |
説明 |
name |
エラーの種類(TypeError、ReferenceErrorなど) |
message |
エラーの説明メッセージ |
stack |
エラーが発生した場所のスタックトレース |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
try {
const numbers = [1, 2, 3];
numbers.push(); // これはエラーにならない
numbers.toUpperCase(); // TypeErrorが発生
} catch (error) {
console.log("エラー名:", error.name);
console.log("メッセージ:", error.message);
console.log("スタックトレース:", error.stack);
}
// 出力:
// エラー名: TypeError
// メッセージ: numbers.toUpperCase is not a function
// スタックトレース: TypeError: numbers.toUpperCase is not a function
// at <anonymous>:4:11
|
catch引数の省略(ES2019以降)#
エラーオブジェクトが不要な場合、ES2019以降ではcatchの引数を省略できます。
1
2
3
4
5
|
try {
JSON.parse("不正なJSON");
} catch {
console.log("JSONのパースに失敗しました");
}
|
finallyブロック#
finallyの役割#
finallyブロックは、tryブロックの処理が成功したか失敗したかに関わらず、必ず実行されるコードを記述するために使います。
1
2
3
4
5
6
7
|
try {
// 処理
} catch (error) {
// エラー処理
} finally {
// 必ず実行される処理
}
|
実践的な使用例#
finallyは、リソースのクリーンアップやローディング表示の制御などに適しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function fetchData() {
console.log("データ取得開始...");
try {
// データ取得処理(エラーが発生する可能性あり)
const data = JSON.parse('{"name": "太郎"}');
console.log("取得成功:", data);
return data;
} catch (error) {
console.log("取得失敗:", error.message);
return null;
} finally {
console.log("データ取得終了(成功・失敗に関わらず実行)");
}
}
fetchData();
// 出力:
// データ取得開始...
// 取得成功: { name: '太郎' }
// データ取得終了(成功・失敗に関わらず実行)
|
finallyとreturnの関係#
finallyブロックは、tryやcatchでreturnが実行された後でも実行されます。ただし、finally内でreturnすると、元の戻り値が上書きされるので注意が必要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function example() {
try {
return "tryの戻り値";
} finally {
console.log("finallyが実行されました");
// return "finallyの戻り値"; // これを有効にすると戻り値が上書きされる
}
}
console.log(example());
// 出力:
// finallyが実行されました
// tryの戻り値
|
JavaScriptのエラーオブジェクト#
組み込みエラーの種類#
JavaScriptには、さまざまな種類の組み込みエラーオブジェクトが用意されています。
| エラー名 |
発生する状況 |
Error |
一般的なエラーの基底クラス |
SyntaxError |
構文が正しくない場合 |
ReferenceError |
存在しない変数を参照した場合 |
TypeError |
値の型が期待と異なる場合 |
RangeError |
値が有効な範囲外の場合 |
URIError |
URI関連の関数で不正な引数を渡した場合 |
EvalError |
eval()関数に関するエラー(現在はほぼ使われない) |
AggregateError |
複数のエラーをまとめる場合(ES2021) |
各エラーの発生例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// ReferenceError - 未定義の変数
try {
console.log(undefinedVariable);
} catch (error) {
console.log(error.name); // ReferenceError
}
// TypeError - 型の不一致
try {
const num = 42;
num.toUpperCase();
} catch (error) {
console.log(error.name); // TypeError
}
// RangeError - 範囲外の値
try {
const arr = new Array(-1);
} catch (error) {
console.log(error.name); // RangeError
}
// SyntaxError - JSON.parseでの構文エラー
try {
JSON.parse("{invalid}");
} catch (error) {
console.log(error.name); // SyntaxError
}
// URIError - 不正なURI
try {
decodeURIComponent("%");
} catch (error) {
console.log(error.name); // URIError
}
|
エラーの継承関係#
すべてのエラーオブジェクトはErrorクラスを継承しています。
classDiagram
Error <|-- SyntaxError
Error <|-- ReferenceError
Error <|-- TypeError
Error <|-- RangeError
Error <|-- URIError
Error <|-- EvalError
Error <|-- AggregateError
class Error {
+name: string
+message: string
+stack: string
}throw文によるエラーの発生#
throw文の基本#
throw文を使うと、意図的にエラーを発生させることができます。これは、関数の引数チェックや、特定の条件でエラーを通知したい場合に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function divide(a, b) {
if (b === 0) {
throw new Error("0で割ることはできません");
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // エラーがthrowされる
} catch (error) {
console.log("エラー:", error.message);
}
// 出力:
// 5
// エラー: 0で割ることはできません
|
任意の値をthrowする#
throwはErrorオブジェクト以外の値もスローできますが、デバッグのためにスタックトレースが取得できるErrorオブジェクトを使うことが推奨されます。
1
2
3
4
5
6
7
8
|
// 非推奨: 文字列をthrow
throw "エラーが発生しました";
// 非推奨: オブジェクトをthrow
throw { code: 404, message: "Not Found" };
// 推奨: Errorオブジェクトをthrow
throw new Error("エラーが発生しました");
|
適切なエラー型を選ぶ#
状況に応じて適切なエラー型を使い分けることで、エラーの原因がより明確になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function processAge(age) {
if (typeof age !== "number") {
throw new TypeError("年齢は数値で指定してください");
}
if (age < 0 || age > 150) {
throw new RangeError("年齢は0から150の範囲で指定してください");
}
return `年齢: ${age}歳`;
}
try {
console.log(processAge("二十歳")); // TypeError
} catch (error) {
if (error instanceof TypeError) {
console.log("型エラー:", error.message);
} else if (error instanceof RangeError) {
console.log("範囲エラー:", error.message);
}
}
|
カスタムエラークラス#
カスタムエラーの作成#
独自のエラークラスを作成することで、アプリケーション固有のエラーを表現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
// 使用例
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ValidationError("メールアドレスの形式が正しくありません");
}
return true;
}
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log("入力エラー:", error.message);
}
}
|
エラークラスの設計パターン#
実務では、エラーコードやHTTPステータスなど、追加情報を持つエラークラスを設計することがあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = "AppError";
this.code = code;
this.statusCode = statusCode;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
statusCode: this.statusCode,
timestamp: this.timestamp,
};
}
}
// 派生エラークラス
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource}が見つかりません`, "NOT_FOUND", 404);
this.name = "NotFoundError";
}
}
class UnauthorizedError extends AppError {
constructor() {
super("認証が必要です", "UNAUTHORIZED", 401);
this.name = "UnauthorizedError";
}
}
// 使用例
try {
throw new NotFoundError("ユーザー");
} catch (error) {
console.log(JSON.stringify(error.toJSON(), null, 2));
}
|
非同期処理でのエラーハンドリング#
Promiseのエラー処理#
Promiseでは、catch()メソッドまたはthen()の第2引数でエラーを処理します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// catch()を使ったエラー処理
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "太郎" });
} else {
reject(new Error("無効なユーザーIDです"));
}
}, 1000);
});
}
fetchUser(-1)
.then((user) => {
console.log("ユーザー:", user);
})
.catch((error) => {
console.log("エラー:", error.message);
});
// 出力:
// エラー: 無効なユーザーIDです
|
Promiseチェーンでのエラー伝播#
Promiseチェーンでは、どこかでエラーが発生すると、最も近いcatch()まで伝播します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
fetchUser(1)
.then((user) => {
console.log("ステップ1: ユーザー取得", user);
throw new Error("ステップ2でエラー発生");
})
.then((result) => {
console.log("ステップ3: この行は実行されない");
})
.catch((error) => {
console.log("キャッチ:", error.message);
})
.then(() => {
console.log("ステップ4: catch後も続行可能");
});
// 出力:
// ステップ1: ユーザー取得 { id: 1, name: '太郎' }
// キャッチ: ステップ2でエラー発生
// ステップ4: catch後も続行可能
|
Promiseチェーンのエラー処理フロー#
flowchart TD
A[Promise開始] --> B{成功?}
B -->|Yes| C[then 1]
B -->|No| F[catch]
C --> D{成功?}
D -->|Yes| E[then 2]
D -->|No| F
E --> G{成功?}
G -->|Yes| H[完了]
G -->|No| F
F --> I[エラー処理]
I --> J[finally]
H --> Jasync/awaitでのエラー処理#
async/awaitを使う場合は、try-catch文でエラーを処理します。同期処理と同じ構文で書けるため、可読性が高くなります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
async function getUserData(id) {
try {
const user = await fetchUser(id);
console.log("ユーザー:", user);
const posts = await fetchUserPosts(user.id);
console.log("投稿:", posts);
return { user, posts };
} catch (error) {
console.log("エラーが発生しました:", error.message);
return null;
} finally {
console.log("処理完了");
}
}
|
非同期処理でよくある間違い#
try-catchは同期的なコードのエラーのみをキャッチします。Promiseをawaitしないと、エラーがキャッチされません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 間違った例: awaitがないためエラーがキャッチされない
async function wrongExample() {
try {
fetchUser(-1); // awaitがない!
console.log("エラーはキャッチされない");
} catch (error) {
console.log("このcatchには到達しない");
}
}
// 正しい例
async function correctExample() {
try {
await fetchUser(-1); // awaitがある
console.log("この行は実行されない");
} catch (error) {
console.log("エラーをキャッチ:", error.message);
}
}
|
複数の非同期処理のエラーハンドリング#
Promise.allでのエラー処理#
Promise.all()は、渡されたPromiseのうち1つでも失敗すると即座に拒否されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then((res) => res.json()),
fetch("/api/posts").then((res) => res.json()),
fetch("/api/comments").then((res) => res.json()),
]);
return { users, posts, comments };
} catch (error) {
console.log("いずれかの取得に失敗:", error.message);
throw error;
}
}
|
Promise.allSettledでのエラー処理#
すべてのPromiseの結果(成功・失敗両方)を取得したい場合は、Promise.allSettled()を使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
async function fetchAllDataSafe() {
const results = await Promise.allSettled([
fetch("/api/users").then((res) => res.json()),
fetch("/api/posts").then((res) => res.json()),
fetch("/api/comments").then((res) => res.json()),
]);
const data = {
users: null,
posts: null,
comments: null,
};
results.forEach((result, index) => {
const keys = ["users", "posts", "comments"];
if (result.status === "fulfilled") {
data[keys[index]] = result.value;
} else {
console.log(`${keys[index]}の取得に失敗:`, result.reason.message);
}
});
return data;
}
|
allとallSettledの比較#
| 項目 |
Promise.all |
Promise.allSettled |
| 動作 |
1つでも失敗すると即座に拒否 |
すべての結果を待つ |
| 戻り値 |
成功時の値の配列 |
結果オブジェクトの配列 |
| 用途 |
すべて成功が必須の場合 |
部分的な失敗を許容する場合 |
実践的なエラーハンドリングパターン#
パターン1: リトライ処理#
ネットワークエラーなど一時的なエラーに対して、リトライを実装するパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
async function fetchWithRetry(url, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message);
if (attempt === maxRetries) {
throw new Error(`${maxRetries}回の試行後も失敗しました`);
}
// 指数バックオフ: 待機時間を徐々に増やす
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
}
}
}
|
パターン2: デフォルト値へのフォールバック#
エラー時にデフォルト値を返すパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
async function getUserPreferences(userId) {
const defaultPreferences = {
theme: "light",
language: "ja",
notifications: true,
};
try {
const response = await fetch(`/api/users/${userId}/preferences`);
if (!response.ok) {
throw new Error("取得失敗");
}
return await response.json();
} catch (error) {
console.log("設定の取得に失敗、デフォルト値を使用:", error.message);
return defaultPreferences;
}
}
|
パターン3: エラーの集約とログ#
複数のエラーを集約し、まとめて処理するパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
class ErrorCollector {
constructor() {
this.errors = [];
}
add(error, context = "") {
this.errors.push({
error,
context,
timestamp: new Date().toISOString(),
});
}
hasErrors() {
return this.errors.length > 0;
}
getErrors() {
return this.errors;
}
clear() {
this.errors = [];
}
report() {
if (!this.hasErrors()) {
console.log("エラーはありません");
return;
}
console.log(`${this.errors.length}件のエラーが発生しました:`);
this.errors.forEach((item, index) => {
console.log(` ${index + 1}. [${item.context}] ${item.error.message}`);
});
}
}
// 使用例
async function processMultipleItems(items) {
const collector = new ErrorCollector();
const results = [];
for (const item of items) {
try {
const result = await processItem(item);
results.push(result);
} catch (error) {
collector.add(error, `アイテム: ${item.id}`);
}
}
collector.report();
return results;
}
|
パターン4: ユーザーフレンドリーなエラーメッセージ#
技術的なエラーをユーザー向けのメッセージに変換するパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
function getUserFriendlyMessage(error) {
// ネットワークエラー
if (error.name === "TypeError" && error.message.includes("fetch")) {
return "インターネット接続を確認してください";
}
// HTTPエラー
if (error.message.includes("HTTP")) {
const status = error.message.match(/\d{3}/)?.[0];
const messages = {
400: "入力内容に問題があります",
401: "ログインが必要です",
403: "アクセス権限がありません",
404: "お探しのページが見つかりません",
500: "サーバーで問題が発生しました",
};
return messages[status] || "エラーが発生しました";
}
// カスタムエラー
if (error instanceof ValidationError) {
return error.message;
}
// デフォルト
return "予期しないエラーが発生しました。しばらくしてから再度お試しください";
}
|
エラーハンドリングのベストプラクティス#
1. エラーは具体的にキャッチする#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 悪い例: すべてのエラーを握りつぶす
try {
doSomething();
} catch (error) {
// 何もしない
}
// 良い例: エラーを適切に処理する
try {
doSomething();
} catch (error) {
console.error("エラー発生:", error);
notifyUser("処理に失敗しました");
}
|
2. 早期リターンでネストを減らす#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 悪い例: 深いネスト
function processData(data) {
if (data) {
if (data.items) {
if (data.items.length > 0) {
// 処理
}
}
}
}
// 良い例: 早期リターン
function processData(data) {
if (!data) {
throw new Error("データがありません");
}
if (!data.items) {
throw new Error("itemsプロパティがありません");
}
if (data.items.length === 0) {
throw new Error("itemsが空です");
}
// 処理
}
|
3. エラーのログは本番環境で適切に#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function handleError(error, context = "") {
// 開発環境: 詳細なエラー情報を表示
if (process.env.NODE_ENV === "development") {
console.error("詳細エラー:", {
message: error.message,
stack: error.stack,
context,
});
}
// 本番環境: 最小限の情報のみログ
if (process.env.NODE_ENV === "production") {
console.error(`Error: ${error.name} - ${context}`);
// 必要に応じてエラー監視サービスに送信
// sendToErrorTracking(error);
}
}
|
まとめ#
JavaScriptのエラーハンドリングについて、基本から実践的なパターンまで解説しました。
| 項目 |
内容 |
| try-catch文 |
同期処理のエラーをキャッチする基本構文 |
| finallyブロック |
成功・失敗に関わらず実行される処理 |
| throw文 |
意図的にエラーを発生させる |
| エラーオブジェクト |
TypeError、ReferenceErrorなど種類を使い分け |
| Promiseのエラー処理 |
catch()メソッドで非同期エラーを処理 |
| async/awaitのエラー処理 |
try-catch文で同期的な記述が可能 |
エラーハンドリングは、ユーザー体験とアプリケーションの安定性を左右する重要な技術です。基本をしっかり押さえ、状況に応じた適切なパターンを選択できるようになりましょう。
参考リンク#