JavaScriptの基礎 - エラーハンドリング

提供: MochiuWiki : SUSE, EC, PCB

📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)

概要

エラーハンドリングは、プログラムの実行中に発生する予期しない問題を検出・処理するための仕組みである。
JavaScriptでは、try / catch / finally 構文を使用して同期処理のエラーを捕捉し、.catch()async/await を使用して非同期処理のエラーを処理する。

適切なエラーハンドリングを実装することにより、以下に示すメリットが得られる。

  • プログラムのクラッシュ防止
    エラーが発生しても処理を継続できる
  • デバッグの効率化
    エラーの原因を特定しやすくなる
  • ユーザー体験の向上
    エラー発生時に適切なフィードバックを提供できる
  • セキュリティの強化
    機密情報を含むスタックトレースの漏洩を防止できる



try / catch / finally

try / catch / finally は、JavaScriptにおける同期処理のエラーハンドリングの基本構文である。
catch または finally のどちらかは省略可能だが、両方を同時に省略することはできない。

基本構文

try ブロック内でエラーが発生すると、処理は即座に catch ブロックに移行する。

finally ブロックは、エラーの有無にかかわらず必ず実行される。

 try {
    // エラーが発生する可能性のある処理
    const result = riskyOperation();
    console.log(result);
 }
 catch (error) {
    // エラーが発生した場合の処理
    console.error('エラーが発生しました:', error.message);
 }
 finally {
    // 成功・失敗にかかわらず常に実行される処理
    console.log('処理が完了しました');
 }


catchブロック

catch ブロックは、try ブロック内で発生したエラーオブジェクトを引数として受け取る。

ES2019以降、エラーオブジェクトが不要な場合は、括弧を省略した オプショナルcatch binding を使用できる。

 // 通常のcatch (エラーオブジェクトを受け取る)
 try {
    JSON.parse('invalid json');
 }
 catch (error) {
    console.error(error.name);    // SyntaxError
    console.error(error.message); // エラーメッセージ
 }
 
 // オプショナルcatch binding (ES2019以降)
 try {
    riskyOperation();
 }
 catch {
    console.error('エラーが発生しました');
 }


エラーの種類に応じて処理を分岐させることもできる。

 try {
    processData(input);
 }
 catch (error) {
    if (error instanceof TypeError) {
       console.error('型エラー:', error.message);
    }
    else if (error instanceof RangeError) {
       console.error('範囲エラー:', error.message);
    }
    else {
       console.error('予期しないエラー:', error.message);
       throw error; // 対処できないエラーは再スロー
    }
 }


finallyブロック

finally ブロックは、trycatch 内に return 文や throw 文があっても必ず実行される。
ファイルのクローズやDB接続の解放など、リソースの後処理に使用する。

 function readFile(filename) {
    let fileHandle = null;
    try {
       fileHandle = openFile(filename);
       return fileHandle.read();
    }
    catch (error) {
       console.error('ファイル読み込みエラー:', error.message);
       return null;
    }
    finally {
       // return や throw の後でも必ず実行される
       if (fileHandle) {
          fileHandle.close();
       }
    }
 }


ネストしたtry / catch

try / catch はネストして使用できる。
内側の catch で処理しきれないエラーは、外側の catch に伝播させることができる。

 try {
    try {
       riskyDatabaseCall();
    }
    catch (dbError) {
       // 内側のcatchでエラーをラップして再スロー
       throw new Error('Database operation failed', { cause: dbError });
    }
 }
 catch (error) {
    // 外側のcatchで最終的なエラー処理
    logError(error);
 }



Errorオブジェクト

JavaScriptには、エラーの種類ごとに専用の組み込みErrorクラスが用意されている。

全ての組み込みエラーは、Error クラスを継承している。

組み込みErrorの種類

下表に、JavaScriptの主な組み込みエラーの種類を示す。

組み込みErrorの種類
エラー型 発生条件
Error 汎用エラー throw new Error('message')
TypeError 型が無効 null.property'str'.forEach()
ReferenceError 未定義変数の参照 console.log(undefinedVar)
SyntaxError 構文エラー eval("let x =;")
RangeError 値が範囲外 new Array(-1)
URIError 無効なURI decodeURIComponent('%')
EvalError eval関連 (後方互換) ほぼ使用されない
AggregateError 複数エラーの集約 Promise.any() で全失敗時


Errorのプロパティ

下表に、Error オブジェクトの主なプロパティを示す。

Errorオブジェクトのプロパティ
プロパティ 説明
name エラー名 (例: "TypeError")
message エラーメッセージ
stack スタックトレース (非標準だがほぼ全環境で利用可能)
cause エラーの原因 (ES2022)


cause プロパティを使用すると、エラーの根本原因を連鎖的に保持できる。

 try {
    // 何らかの処理
 }
 catch (originalError) {
    const wrappedError = new Error('処理に失敗しました', { cause: originalError });
    console.error(wrappedError.cause); // 元のエラーにアクセス可能
    throw wrappedError;
 }



throw文

throw 文は、任意の値を例外としてスローするための構文である。

スローされた値は、呼び出しスタックを遡って最初の catch ブロックで捕捉される。

基本構文

throw 文の基本的な使い方を以下に示す。

 // 文字列をスロー (非推奨)
 throw 'エラーが発生しました';
 
 // Errorオブジェクトをスロー (推奨)
 throw new Error('エラーが発生しました');
 
 // 条件に基づいてスロー
 function divide(a, b) {
    if (b === 0) {
       throw new Error('ゼロ除算はできません');
    }
 
    return a / b;
 }


文字列や数値等、任意の値をスローすることは技術的には可能だが、Error オブジェクトのスローが強く推奨される。
その理由は以下の通りである。

  • スタックトレースの自動キャプチャ
    エラー発生箇所の追跡が容易になる。
  • 標準プロパティ (name, message, stack) の提供
    エラー情報へのアクセス方法が統一される。
  • デバッガやエラーモニタリングツールとの互換性
    Sentry等のツールが正しく認識できる。


Errorオブジェクトのスロー

組み込みエラーを適切に使い分けることで、エラーの原因を明確に伝えることができる。

 function processUserInput(input) {
    if (typeof input !== 'string') {
       throw new TypeError('inputは文字列である必要があります');
    }
 
    if (input.length === 0) {
       throw new RangeError('inputは1文字以上である必要があります');
    }
 
    if (!isKnownVariable(input)) {
       throw new ReferenceError(`"${input}" は定義されていません`);
    }
 
    return input.trim();
 }
 
 try {
    processUserInput(42);
 }
 catch (error) {
    console.error(`${error.name}: ${error.message}`);
    // TypeError: inputは文字列である必要があります
 }



カスタムエラークラス

組み込みエラーに加え、アプリケーション固有のエラーを表現するためにカスタムエラークラスを作成することができる。

カスタムエラークラスは、Error クラスを継承して定義する。

基本的なカスタムエラー

最もシンプルなカスタムエラークラスの定義方法を以下に示す。

 class ValidationError extends Error {
    constructor(message) {
       super(message);
       this.name = 'ValidationError';
    }
 }
 
 // 使用例
 function validateAge(age) {
    if (age < 0 || age > 150) {
       throw new ValidationError(`年齢の値が不正です: ${age}`);
    }
 
    return age;
 }
 
 try {
    validateAge(-5);
 }
 catch (error) {
    if (error instanceof ValidationError) {
       console.error('バリデーションエラー:', error.message);
    }
 }


実用的なカスタムエラー

追加プロパティを持つカスタムエラーや、エラー階層の構築例を以下に示す。

 // HTTPエラーの基底クラス
 class HTTPError extends Error {
    constructor(statusCode, message) {
       super(message);
       this.name = 'HTTPError';
       this.statusCode = statusCode;
    }
 }
 
 // 404 Not Found専用エラー
 class NotFoundError extends HTTPError {
    constructor(resource) {
       super(404, `${resource} not found`);
       this.name = 'NotFoundError';
    }
 }
 
 // 403 Forbidden専用エラー
 class ForbiddenError extends HTTPError {
    constructor(resource) {
       super(403, `Access to ${resource} is forbidden`);
       this.name = 'ForbiddenError';
    }
 }
 
 // 使用例
 function fetchResource(path, user) {
    if (!resourceExists(path)) {
       throw new NotFoundError(path);
    }
 
    if (!user.hasPermission(path)) {
       throw new ForbiddenError(path);
    }
    return getResource(path);
 }
 
 try {
    fetchResource('/admin/data', currentUser);
 }
 catch (error) {
    if (error instanceof NotFoundError) {
       res.status(404).send(error.message);
    }
    else if (error instanceof ForbiddenError) {
       res.status(403).send(error.message);
    }
    else if (error instanceof HTTPError) {
       res.status(error.statusCode).send(error.message);
    }
    else {
       res.status(500).send('内部サーバーエラー');
    }
 }


ES2022の Error.prototype.cause を利用すると、エラーの根本原因を連鎖的に保持できる。

 async function connectDatabase(config) {
    try {
       await db.connect(config);
    }
    catch (originalError) {
       throw new Error('データベース接続に失敗しました', { cause: originalError });
    }
 }
 
 try {
    await connectDatabase(config);
 }
 catch (error) {
    console.error(error.message);       // データベース接続に失敗しました
    console.error(error.cause.message); // 元のエラーメッセージ
 }



非同期処理のエラーハンドリング

非同期処理では、同期処理とは異なるエラーハンドリングの方法が必要になる。

Promiseベースの処理には .catch() メソッドを、async / await には try / catch を使用する。

Promiseのエラー捕捉

Promiseのエラーは、.catch() メソッドで捕捉する。

.then() の第2引数でも捕捉できるが、成功ハンドラ内のエラーを捕捉できないため非推奨である。

 // .catch()を使用した方法 (推奨)
 fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('エラー:', error.message));
 
 // .then()の第2引数を使用した方法 (非推奨)
 // 成功ハンドラ (第1引数) 内のエラーを捕捉できない
 fetch('/api/data')
    .then(
       data => console.log(data),
       error => console.error('エラー:', error.message)
    );


async / awaitのエラー捕捉

async/await を使用した非同期処理では、try / catch でエラーを捕捉できる。

 async function fetchUserData(userId) {
    try {
       const response = await fetch(`/api/users/${userId}`);
 
       if (!response.ok) {
          throw new HTTPError(response.status, `HTTPエラー: ${response.status}`);
       }
 
       return await response.json();
    }
    catch (error) {
       console.error('データの取得に失敗しました:', error.message);
       throw error; // 必要に応じて呼び出し元に再スロー
    }
 }
 
 // 呼び出し側
 async function main() {
    try {
       const user = await fetchUserData(123);
       console.log(user);
    }
    catch (error) {
       console.error('ユーザデータを取得できませんでした');
    }
 }


複数の非同期処理のエラー捕捉

複数の非同期処理を並列実行する場合、Promise.allPromise.allSettledPromise.anyPromise.race の違いに注意が必要である。

下表に各メソッドの特性を示す。

複数の非同期処理メソッドの比較
メソッド 成功条件 失敗条件 返却値 用途
Promise.all 全て成功 1つでも失敗 結果の配列 全て成功が必要な場合
Promise.allSettled 常に完了 なし {status, value/reason} の配列 全結果を個別確認する場合
Promise.any 1つでも成功 全て失敗 最初の成功値 フォールバックが必要な場合
Promise.race 最速の完了 最速の完了 最初の完了値 タイムアウト処理


各メソッドの使用例を以下に示す。

 const p1 = fetch('/api/users');
 const p2 = fetch('/api/posts');
 const p3 = fetch('/api/comments');
 
 // Promise.all: 1つでも失敗すると全体が失敗
 try {
    const [users, posts, comments] = await Promise.all([p1, p2, p3]);
 }
 catch (error) {
    console.error('いずれかのリクエストが失敗:', error.message);
 }
 
 // Promise.allSettled: 全ての結果を個別に確認できる
 const results = await Promise.allSettled([p1, p2, p3]);
 results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
       console.log(`リクエスト${index + 1}成功:`, result.value);
    }
    else {
       console.error(`リクエスト${index + 1}失敗:`, result.reason);
    }
 });
 
 // Promise.any: 最初に成功したものを使用 (全て失敗するとAggregateError)
 try {
    const data = await Promise.any([
       fetch('/api/primary'),
       fetch('/api/fallback1'),
       fetch('/api/fallback2')
    ]);
 }
 catch (error) {
    // AggregateError: error.errors に個別のエラーが格納される
    console.error('全てのリクエストが失敗:', error.errors);
 }



エラーハンドリングのパターン

ガード節パターン

ガード節パターンは、早期リターンを使用して条件のネストを浅くするパターンである。

バリデーションや前提条件のチェックに有効で、ソースコードの可読性が向上する。

ガード節を使用しない場合と使用する場合の比較を以下に示す。

 // ガード節なし (深いネスト)
 function processUser(user) {
    if (user) {
       if (user.isActive) {
          if (user.email) {
             sendEmail(user.email);
             return true;
          }
          else {
             console.error('メールアドレスがありません');
             return false;
          }
       }
       else {
          console.error('ユーザーがアクティブではありません');
          return false;
       }
    }
    else {
       console.error('ユーザーが存在しません');
       return false;
    }
 }
 
 // ガード節あり (フラットな構造)
 function processUser(user) {
    if (!user) {
       console.error('ユーザーが存在しません');
       return false;
    }
 
    if (!user.isActive) {
       console.error('ユーザーがアクティブではありません');
       return false;
    }
 
    if (!user.email) {
       console.error('メールアドレスがありません');
       return false;
    }
 
    sendEmail(user.email);
    return true;
 }


Resultパターン

Resultパターンは、throw を使用せず、成功・失敗を表すオブジェクトを返すパターンである。

{ success, data, error } のような形式で結果を返すことにより、呼び出し側が明示的にエラーを処理するよう強制できる。

 // Resultパターンの実装
 async function fetchUser(userId) {
    try {
       const response = await fetch(`/api/users/${userId}`);
       if (!response.ok) {
          return {
             success: false,
             data: null,
             error: `HTTPエラー: ${response.status}`
          };
       }
       const data = await response.json();
 
       return { success: true, data, error: null };
    }
    catch (error) {
       return { success: false, data: null, error: error.message };
    }
 }
 
 // 呼び出し側
 async function main() {
    const result = await fetchUser(123);
 
    if (!result.success) {
       console.error('取得失敗:', result.error);
       return;
    }
 
    console.log('取得成功:', result.data);
 }


下表に、Resultパターンの主なメリットを示す。

Resultパターンのメリット
メリット 説明
エラーの明示的な処理 呼び出し側がエラーチェックを忘れにくい。
例外の伝播を防止 意図しない場所でのエラー処理を避けられる。
TypeScriptとの相性が良い 型定義によりエラー処理を型安全に行える。



関連情報