はじめに

プログラムを書いていると、必ずといっていいほどエラーに遭遇します。ネットワーク接続の失敗、ユーザーの不正な入力、存在しないファイルへのアクセスなど、エラーの原因は多岐にわたります。

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ブロックは、trycatchreturnが実行された後でも実行されます。ただし、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する

throwErrorオブジェクト以外の値もスローできますが、デバッグのためにスタックトレースが取得できる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 --> J

async/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文で同期的な記述が可能

エラーハンドリングは、ユーザー体験とアプリケーションの安定性を左右する重要な技術です。基本をしっかり押さえ、状況に応じた適切なパターンを選択できるようになりましょう。

参考リンク