TypeScriptにおけるPromiseとasync/awaitのエラーハンドリング完全ガイド

TypeScriptにおける非同期処理は、複雑な処理をシンプルに扱うために非常に重要です。特に、APIからのデータ取得やファイルの読み書きなど、外部リソースとの通信を伴う操作では、非同期処理が欠かせません。この非同期処理に対して適切なエラーハンドリングを行わないと、予期せぬエラーが発生し、アプリケーションの動作が不安定になることがあります。

本記事では、TypeScriptでよく使われる非同期処理の2つの方法、Promiseasync/awaitに焦点を当て、それぞれのエラーハンドリング方法を詳しく解説します。エラーハンドリングの基本的な考え方から、実際にプロジェクトで活用できるベストプラクティス、さらにはリソースクリーンアップや型システムを利用したエラー管理についても説明していきます。

次にPromiseによるエラーハンドリングについて解説します。

目次
  1. Promiseによるエラーハンドリング
    1. Promiseの基本構造
    2. Promiseのエラーハンドリング方法
  2. async/awaitによるエラーハンドリング
    1. async/awaitの基本構造
    2. async/awaitのエラーハンドリング方法
    3. Promiseとの比較
  3. 非同期処理におけるエラーハンドリングの課題
    1. Promiseチェーンにおけるエラーの課題
    2. async/awaitにおけるエラーの課題
    3. 非同期処理の失敗が見落とされる問題
  4. Promiseのcatchメソッドを活用したパターン
    1. catchメソッドの基本的な使い方
    2. 中間のPromiseでのエラーハンドリング
    3. 複数のcatchを使ったエラーハンドリング
    4. catchの再スローによるエラーチェーンの構築
  5. async/awaitとtry/catchの応用例
    1. 複数の非同期処理のエラーハンドリング
    2. 並行処理のエラーハンドリング
    3. 特定の非同期処理だけエラーハンドリングするパターン
    4. async/awaitによる再スローの活用
  6. 非同期関数のエラーを一括管理する方法
    1. エラーハンドリング用のラッパー関数を作成
    2. カスタムエラーハンドラーの導入
    3. エラーを含む結果を返すパターン
  7. finallyによるリソースのクリーンアップ
    1. Promiseにおけるfinallyの使用例
    2. async/awaitにおけるfinallyの使用例
    3. finallyの典型的な使用シナリオ
  8. エラーハンドリングのベストプラクティス
    1. 1. すべての非同期処理にエラーハンドリングを実装する
    2. 2. エラーをユーザーフレンドリーにする
    3. 3. 共通のエラーハンドリング関数を作成する
    4. 4. 特定のエラーパターンに対応する
    5. 5. エラーを再スローする
    6. 6. TypeScriptの型システムを活用する
  9. TypeScriptの型システムを活用したエラーハンドリング
    1. 1. エラー型の定義
    2. 2. エラーハンドリングにおける型アノテーションの活用
    3. 3. カスタム型を使ったエラーハンドリング
    4. 4. エラーの型ガードを利用する
    5. 5. エラーオブジェクトに追加情報を付加する
  10. 非同期処理のエラーハンドリングに関する演習問題
    1. 演習1: 基本的なPromiseのエラーハンドリング
    2. 演習2: async/awaitとtry/catchのエラーハンドリング
    3. 演習3: 型を活用したエラーハンドリング
    4. 演習4: カスタムエラークラスの作成
  11. まとめ

Promiseによるエラーハンドリング

Promiseは、JavaScriptやTypeScriptにおける非同期処理を扱うための基本的な構造です。Promiseは、処理が成功したか失敗したかを表すオブジェクトを返し、それに応じたアクションを実行することができます。Promiseでのエラーハンドリングは、thencatchメソッドを使って行います。

Promiseの基本構造

Promiseには3つの状態があります:

  1. pending(処理中)
  2. fulfilled(処理成功)
  3. rejected(処理失敗)

Promiseがrejected状態になった場合、エラーハンドリングが必要です。エラーはcatchメソッドを使って捕捉できます。

Promiseのエラーハンドリング方法

以下の例は、Promiseを使ったエラーハンドリングの基本的なコードです。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('データ取得成功');
    } else {
      reject('エラー: データ取得に失敗しました');
    }
  });
};

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

このコードでは、fetchData関数がデータを取得し、成功した場合はresolve、失敗した場合はrejectを呼び出します。thenで成功時の処理を、catchでエラー処理を行います。

Promiseでは、非同期処理の失敗を適切にcatchメソッドで処理することで、エラーの発生に対してアプリケーションが予期しないクラッシュを防げます。

次に、async/awaitによるエラーハンドリングについて説明します。

async/awaitによるエラーハンドリング

async/awaitは、Promiseをさらに扱いやすくするために導入された構文で、より同期的なコードのように非同期処理を記述できます。Promiseチェーンを使わずに、直感的にコードを記述できるため、読みやすさと保守性が向上します。特に、非同期処理におけるエラーハンドリングは、try/catch構文を使用することで、コードがシンプルかつ強力になります。

async/awaitの基本構造

asyncキーワードを付けた関数は常にPromiseを返します。そして、awaitキーワードを使うことで、そのPromiseが解決するまで待機し、結果を得ることができます。この構造は、非同期処理の成功・失敗を同期的なスタイルで扱えるため、エラーハンドリングも直感的です。

async/awaitのエラーハンドリング方法

以下は、async/awaitを使ったエラーハンドリングの例です。

const fetchDataAsync = async () => {
  try {
    const success = Math.random() > 0.5;
    if (success) {
      return 'データ取得成功';
    } else {
      throw new Error('エラー: データ取得に失敗しました');
    }
  } catch (error) {
    console.error(error.message);
    // エラーを再度スローすることも可能
    throw error;
  }
};

const processData = async () => {
  try {
    const data = await fetchDataAsync();
    console.log(data);
  } catch (error) {
    console.error('処理中にエラーが発生しました:', error.message);
  }
};

processData();

この例では、fetchDataAsync関数がエラーをスローした場合、processData関数でそれを捕捉します。try/catchを用いることで、エラーが発生した場合に適切な処理を行うことができます。また、catchブロック内でエラーを再スローして、さらなるエラーハンドリングを上位の関数に委ねることも可能です。

Promiseとの比較

Promiseのcatchメソッドによるエラーハンドリングは、非同期処理が完了した後にエラーチェーンで捕捉されるのに対し、async/awaitではtry/catchによってコードの中でエラーを即座に処理できます。このため、エラー発生箇所と処理箇所がより直感的に見える点が、async/awaitの大きな利点です。

次に、非同期処理におけるエラーハンドリングの課題について解説します。

非同期処理におけるエラーハンドリングの課題

非同期処理におけるエラーハンドリングは便利な一方で、複数の処理が並行して実行されるため、エラーの検出や管理が難しくなることがあります。Promiseやasync/awaitを使用する際、エラーの特性や、どのように処理を設計するかによって、さまざまな課題に直面します。これらの課題を理解し、適切に対処することで、より堅牢な非同期処理が実現します。

Promiseチェーンにおけるエラーの課題

Promiseチェーンでは、複数の非同期処理を連鎖的に実行することができますが、各処理の途中でエラーが発生すると、その後のPromiseが実行されず、エラーハンドリングが難しくなることがあります。例えば、以下のようなPromiseチェーンでは、どこでエラーが発生したのかを特定するのが難しくなることがあります。

fetchData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .catch((error) => console.error('エラーが発生:', error));

このコードでは、どのステップでエラーが発生したかの詳細を得るのが難しくなります。また、すべての処理が同じcatchブロックで処理されるため、エラーの内容に応じた対処がしづらいです。

async/awaitにおけるエラーの課題

async/awaitでは、try/catchブロック内でエラーを処理できますが、複数の非同期処理が並列で行われる場合、エラーの捕捉が複雑になる場合があります。例えば、以下のように複数の非同期関数を並列に実行する場合、それぞれのエラーハンドリングを個別に行う必要があります。

const fetchAndProcessData = async () => {
  try {
    const data = await fetchData();
    const processedData = await processData(data);
  } catch (error) {
    console.error('データの取得または処理中にエラーが発生しました:', error.message);
  }
};

この例では、fetchDataprocessDataのどちらでエラーが発生したのかは分かりません。これに対処するためには、各関数ごとにtry/catchを使ってエラーを分離し、処理を細かく管理する必要があります。

非同期処理の失敗が見落とされる問題

非同期処理のエラーハンドリングが適切に行われていないと、エラーが発生しても気づかない、もしくはアプリケーションが予期せぬ挙動を示す可能性があります。特に、Promiseの中でエラーをスローしても、それがキャッチされなければエラーが見逃されてしまいます。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;
    if (!success) {
      // エラーをスローするが、ハンドリングされない
      throw new Error('非同期処理中にエラーが発生しました');
    }
    resolve('データ取得成功');
  });
};

このようなケースでは、Promiseチェーンやtry/catchでエラーを適切に捕捉しないと、エラーが表に出てこないことがあります。

次に、Promiseのcatchメソッドを活用した具体的なエラーハンドリングパターンを紹介します。

Promiseのcatchメソッドを活用したパターン

Promiseのエラーハンドリングにおいて、catchメソッドは非常に重要な役割を果たします。非同期処理がrejected状態になると、自動的にcatchメソッドが呼び出され、エラーを適切に処理できます。Promiseチェーンを構築する際、このcatchメソッドを有効に活用することで、柔軟で直感的なエラーハンドリングが可能になります。

catchメソッドの基本的な使い方

catchメソッドは、Promiseが失敗した場合にエラーハンドリングを行うための関数を登録します。以下のコードは、Promiseチェーン内で発生するエラーをキャッチして処理する基本的な例です。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('データ取得成功');
    } else {
      reject('エラー: データ取得に失敗しました');
    }
  });
};

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error('エラーハンドリング:', error);
  });

この例では、fetchData関数がエラーをスローした場合、Promiseチェーン内のcatchメソッドによってエラーが捕捉されます。catchは、Promiseチェーンの最後に置くのが基本で、全体の非同期処理に対する一括的なエラーハンドリングを実現できます。

中間のPromiseでのエラーハンドリング

Promiseチェーンの途中でエラーが発生した場合、その後のPromiseはスキップされ、最初に出会ったcatchメソッドに処理が渡されます。これは、長い非同期処理の途中でエラーが発生しても、エラー処理を一箇所で統一できる利点があります。

fetchData()
  .then((data) => {
    // データが取得できたが、次の処理でエラーが発生する
    throw new Error('処理中にエラーが発生しました');
  })
  .then((processedData) => {
    // このthenはスキップされる
    console.log(processedData);
  })
  .catch((error) => {
    // ここで全てのエラーを処理
    console.error('全体のエラーハンドリング:', error.message);
  });

この例では、最初のthenでエラーが発生すると、次のthenはスキップされて、catchが実行されます。これにより、Promiseチェーン全体のエラーハンドリングを簡潔に行えます。

複数のcatchを使ったエラーハンドリング

場合によっては、Promiseチェーンの異なる部分で異なるエラーハンドリングが必要になることがあります。複数のcatchメソッドを使うことで、特定の処理ごとに個別のエラーハンドリングを行うことが可能です。

fetchData()
  .then((data) => {
    return processData(data);
  })
  .catch((error) => {
    console.error('データの処理中にエラーが発生:', error.message);
  })
  .then(() => {
    return saveData();
  })
  .catch((error) => {
    console.error('データ保存中にエラーが発生:', error.message);
  });

この例では、データの処理段階と保存段階それぞれで個別にエラーハンドリングを行っています。これにより、エラーが発生した段階に応じた適切な対処が可能となります。

catchの再スローによるエラーチェーンの構築

catchメソッド内でエラーを再スローすることも可能です。これにより、複数段階のエラーハンドリングが必要な場合でも、上位の処理でまとめて対応することができます。

fetchData()
  .then((data) => {
    return processData(data);
  })
  .catch((error) => {
    console.error('処理中のエラー:', error.message);
    throw new Error('処理段階でエラーが発生しました');
  })
  .then(() => {
    return saveData();
  })
  .catch((error) => {
    console.error('最終エラーハンドリング:', error.message);
  });

このパターンでは、最初のcatchでエラーを捕捉し、再度スローすることで、最終的なエラーハンドリングを統一できます。

次に、async/awaitとtry/catchの応用例について説明します。

async/awaitとtry/catchの応用例

async/await構文は、非同期処理をより直感的に扱える一方、エラーハンドリングもtry/catchを利用して、Promiseチェーンよりもシンプルに記述できます。特に、複数の非同期処理が関わる場合や、複雑なエラー管理が必要な場合には、async/awaitとtry/catchを組み合わせた応用例が役立ちます。

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

複数の非同期処理を実行する際、各処理で個別のエラーハンドリングを行いながら、全体的な制御を保つことが重要です。以下の例は、複数の非同期処理を順番に実行し、それぞれのエラーハンドリングをtry/catchで行う方法です。

const fetchDataAsync = async () => {
  const success = Math.random() > 0.5;
  if (success) {
    return 'データ取得成功';
  } else {
    throw new Error('データ取得に失敗しました');
  }
};

const processDataAsync = async (data: string) => {
  if (!data) {
    throw new Error('処理に必要なデータがありません');
  }
  return `処理済みデータ: ${data}`;
};

const saveDataAsync = async (data: string) => {
  const success = Math.random() > 0.5;
  if (!success) {
    throw new Error('データ保存に失敗しました');
  }
  return 'データ保存成功';
};

const handleAsyncProcess = async () => {
  try {
    const data = await fetchDataAsync();
    console.log('データ取得:', data);

    const processedData = await processDataAsync(data);
    console.log('データ処理:', processedData);

    const saveResult = await saveDataAsync(processedData);
    console.log(saveResult);
  } catch (error) {
    console.error('エラー発生:', error.message);
  }
};

handleAsyncProcess();

このコードでは、データの取得、処理、保存の3つの非同期操作を順次実行し、各操作でエラーが発生した場合にcatchブロックで処理されます。これにより、エラーの発生箇所を特定し、適切に対応することができます。

並行処理のエラーハンドリング

複数の非同期処理を並行して実行する場合、Promise.allを使用することで効率よく処理できます。ただし、1つのPromiseが失敗すると他のPromiseも中断されるため、すべての処理が終了するまでエラーハンドリングを待つ必要があります。

const handleMultipleProcesses = async () => {
  try {
    const results = await Promise.all([fetchDataAsync(), processDataAsync('データ')]);
    console.log('並行処理結果:', results);
  } catch (error) {
    console.error('並行処理中にエラーが発生しました:', error.message);
  }
};

handleMultipleProcesses();

この例では、fetchDataAsyncprocessDataAsyncを並行して実行しています。どちらか一方でエラーが発生した場合、catchブロックでそのエラーを捕捉し、処理を停止します。

特定の非同期処理だけエラーハンドリングするパターン

すべての非同期処理をtry/catchでラップすると、コードが冗長になりがちです。そのため、特定の非同期処理に対してのみエラーハンドリングを行い、他の処理は共通のtry/catchブロックで扱うことも有効です。

const handleSpecificError = async () => {
  try {
    const data = await fetchDataAsync();
    console.log('データ取得:', data);

    try {
      const processedData = await processDataAsync(data);
      console.log('データ処理:', processedData);
    } catch (processError) {
      console.error('データ処理中にエラー:', processError.message);
    }

    const saveResult = await saveDataAsync(data);
    console.log('データ保存:', saveResult);
  } catch (error) {
    console.error('全体のエラー:', error.message);
  }
};

handleSpecificError();

この例では、processDataAsync内で発生するエラーのみ個別にハンドリングし、その他のエラーは外側のtry/catchで一括して処理しています。これにより、特定の処理で発生するエラーに対して個別の対応が可能になります。

async/awaitによる再スローの活用

複数段階の非同期処理でエラーを再スローすることで、上位レイヤーでまとめてエラーを処理することもできます。

const handleAsyncProcessWithRethrow = async () => {
  try {
    const data = await fetchDataAsync();
    const processedData = await processDataAsync(data);
    const saveResult = await saveDataAsync(processedData);
    return saveResult;
  } catch (error) {
    console.error('処理中にエラーが発生しました。エラーを再スローします:', error.message);
    throw error; // エラーを再スロー
  }
};

const mainProcess = async () => {
  try {
    const result = await handleAsyncProcessWithRethrow();
    console.log('最終結果:', result);
  } catch (error) {
    console.error('最上位のエラーハンドリング:', error.message);
  }
};

mainProcess();

この例では、handleAsyncProcessWithRethrow関数で発生したエラーを再スローし、上位の関数で最終的にエラーをキャッチして処理しています。これにより、全体のエラーハンドリングを統一的に管理できます。

次に、非同期関数のエラーを一括管理する方法について説明します。

非同期関数のエラーを一括管理する方法

非同期処理が多くなると、それぞれの関数で個別にエラーハンドリングを行うことが煩雑になります。このような状況に対応するために、非同期関数のエラーを一括管理するための方法を考える必要があります。以下では、非同期関数をラップして、すべてのエラーを一元的に管理するテクニックをいくつか紹介します。

エラーハンドリング用のラッパー関数を作成

各非同期関数をエラーハンドリング付きでラップすることで、コードの重複を避けつつ、一貫したエラーハンドリングを行うことができます。この方法は、非同期関数が多数存在する大規模なプロジェクトにおいて特に有効です。

以下は、ラッパー関数を使用して、エラーを統一的に処理する方法の例です。

const handleAsyncError = async (asyncFunc: Function) => {
  try {
    return await asyncFunc();
  } catch (error) {
    console.error('エラーハンドリング中:', error.message);
    throw error; // 必要に応じて再スローする
  }
};

const fetchDataAsync = async () => {
  // データを取得する処理
  return 'データ取得成功';
};

const processDataAsync = async (data: string) => {
  // データを処理する処理
  return `処理済みデータ: ${data}`;
};

const saveDataAsync = async (data: string) => {
  // データを保存する処理
  return 'データ保存成功';
};

// ラッパーを使った一括エラーハンドリング
const mainProcess = async () => {
  try {
    const data = await handleAsyncError(fetchDataAsync);
    const processedData = await handleAsyncError(() => processDataAsync(data));
    const saveResult = await handleAsyncError(() => saveDataAsync(processedData));
    console.log(saveResult);
  } catch (error) {
    console.error('メインプロセス中のエラー:', error.message);
  }
};

mainProcess();

この例では、handleAsyncError関数を使って、各非同期関数のエラーハンドリングを統一的に管理しています。これにより、エラーハンドリングのロジックを各関数に埋め込むことなく、一元的に処理できます。また、handleAsyncError関数は、必要に応じてエラーを再スローするため、上位の関数でさらに詳細なエラーハンドリングも可能です。

カスタムエラーハンドラーの導入

大規模なプロジェクトでは、エラーを単にログ出力するだけでなく、エラーの内容に応じて異なる対処を行うことが求められる場合があります。このようなケースでは、カスタムエラーハンドラーを導入し、エラーの種類に応じた処理を柔軟に実装することが有効です。

class CustomErrorHandler {
  static handleError(error: any) {
    if (error instanceof TypeError) {
      console.error('型エラー:', error.message);
    } else if (error instanceof NetworkError) {
      console.error('ネットワークエラー:', error.message);
    } else {
      console.error('一般的なエラー:', error.message);
    }
    // 追加のエラーログや通知処理をここで行う
  }
}

const handleAsyncWithCustomErrorHandler = async (asyncFunc: Function) => {
  try {
    return await asyncFunc();
  } catch (error) {
    CustomErrorHandler.handleError(error);
    throw error;
  }
};

// 使用例
const mainProcessWithCustomHandler = async () => {
  try {
    const data = await handleAsyncWithCustomErrorHandler(fetchDataAsync);
    const processedData = await handleAsyncWithCustomErrorHandler(() => processDataAsync(data));
    const saveResult = await handleAsyncWithCustomErrorHandler(() => saveDataAsync(processedData));
    console.log(saveResult);
  } catch (error) {
    console.error('全体のエラーハンドリング:', error.message);
  }
};

mainProcessWithCustomHandler();

この例では、CustomErrorHandlerクラスを作成し、エラーの種類に応じたハンドリングを行っています。たとえば、ネットワークエラーや型エラーなど、異なるタイプのエラーに対して個別の処理を実行できます。また、エラーログの出力だけでなく、メール通知やアラートの送信など、追加のエラーハンドリング処理もカスタマイズ可能です。

エラーを含む結果を返すパターン

非同期処理が失敗するたびに例外をスローするのではなく、エラー情報を含む結果を返すパターンもあります。これにより、エラーハンドリングを行いつつ、処理を続行させることができます。

const handleErrorResult = async (asyncFunc: Function) => {
  try {
    const result = await asyncFunc();
    return { success: true, result };
  } catch (error) {
    return { success: false, error };
  }
};

const mainProcessWithResultHandling = async () => {
  const fetchDataResult = await handleErrorResult(fetchDataAsync);
  if (!fetchDataResult.success) {
    console.error('データ取得中のエラー:', fetchDataResult.error.message);
    return;
  }

  const processedDataResult = await handleErrorResult(() => processDataAsync(fetchDataResult.result));
  if (!processedDataResult.success) {
    console.error('データ処理中のエラー:', processedDataResult.error.message);
    return;
  }

  const saveDataResult = await handleErrorResult(() => saveDataAsync(processedDataResult.result));
  if (!saveDataResult.success) {
    console.error('データ保存中のエラー:', saveDataResult.error.message);
  } else {
    console.log('データ保存成功:', saveDataResult.result);
  }
};

mainProcessWithResultHandling();

このパターンでは、成功か失敗かを示すフラグを含むオブジェクトを返し、エラーが発生しても処理の継続や異なる対応ができるようにしています。これにより、エラーハンドリングを柔軟に行いながら、アプリケーションの動作を制御できます。

次に、finallyによるリソースのクリーンアップについて説明します。

finallyによるリソースのクリーンアップ

非同期処理においては、エラーハンドリングとともに、リソースの解放や後処理を適切に行うことが重要です。たとえば、ファイルのクローズ、ネットワークリクエストのキャンセル、メモリの解放など、エラーが発生した場合でも必ず実行したい処理が存在します。こうしたリソース管理を確実に行うために、finallyブロックが利用されます。

finallyは、Promiseやasync/awaitのどちらでも利用でき、エラーの有無にかかわらず、最終的に必ず実行されるコードを記述できます。

Promiseにおけるfinallyの使用例

Promiseチェーンでは、finallyを使って、成功・失敗に関わらず実行したい処理を明示的に記述することができます。以下のコードは、Promise内でのリソースクリーンアップの基本的な使用例です。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('データ取得成功');
    } else {
      reject('エラー: データ取得に失敗しました');
    }
  });
};

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log('リソースを解放しました');
  });

この例では、Promiseチェーンの最後にfinallyを追加し、エラーが発生してもfinallyブロックでリソースの解放処理が必ず実行されます。これにより、たとえばネットワーク接続の終了や一時ファイルの削除といった後処理を確実に行えます。

async/awaitにおけるfinallyの使用例

async/await構文でも、try/catchブロック内でfinallyを使って、例外が発生しても必ずリソースをクリーンアップすることが可能です。

const fetchDataAsync = async () => {
  const success = Math.random() > 0.5;
  if (success) {
    return 'データ取得成功';
  } else {
    throw new Error('エラー: データ取得に失敗しました');
  }
};

const processAsync = async () => {
  try {
    const data = await fetchDataAsync();
    console.log('データ処理成功:', data);
  } catch (error) {
    console.error('データ処理中にエラーが発生:', error.message);
  } finally {
    console.log('リソースを解放しました');
  }
};

processAsync();

この例では、try/catchブロックの後にfinallyを追加し、エラーが発生した場合でも、最後に必ずリソースのクリーンアップ処理が実行されます。これにより、非同期処理の状態に依存せず、安定した後処理が保証されます。

finallyの典型的な使用シナリオ

finallyは、以下のような場面でよく使用されます:

  • ネットワークリクエストのクリーンアップ:リクエストの終了処理やタイムアウト時のキャンセル処理
  • ファイルのクローズ:ファイル操作後にファイルを確実に閉じる
  • メモリリソースの解放:大規模なオブジェクトやデータバッファのクリア
  • ログ出力:非同期処理の終了時に、操作の成功や失敗に関わらずログを記録する

これらの場面では、finallyを使うことで、エラーが発生したとしても確実にリソース管理や後処理を実行できるため、コードの安全性と安定性を向上させることができます。

次に、エラーハンドリングのベストプラクティスについて解説します。

エラーハンドリングのベストプラクティス

非同期処理におけるエラーハンドリングは、システムの安定性と保守性に大きく影響します。Promiseやasync/awaitを活用したエラーハンドリングには、多くのテクニックがあり、これらを適切に実装することで予期しない動作やクラッシュを防ぐことができます。ここでは、TypeScriptで非同期処理を行う際のエラーハンドリングにおけるベストプラクティスをいくつか紹介します。

1. すべての非同期処理にエラーハンドリングを実装する

非同期処理は、正常に終了することが期待されますが、外部の要因(ネットワーク障害、データ不整合など)で失敗する可能性が常にあります。非同期関数を呼び出すたびに、適切なエラーハンドリングを必ず実装することが重要です。

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('データ取得に失敗しました');
    }
    return await response.json();
  } catch (error) {
    console.error('エラー発生:', error.message);
  }
};

上記の例では、fetchリクエストが失敗した場合でも、catchでエラーを捕捉し、処理を適切に制御しています。

2. エラーをユーザーフレンドリーにする

エラーハンドリングの目的は、ただエラーをキャッチするだけではなく、ユーザーに対して適切なフィードバックを提供することです。エラーの内容をユーザー向けに変換し、わかりやすいメッセージを提示することで、ユーザーエクスペリエンスを向上させることができます。

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('サーバーエラーが発生しました。後ほど再試行してください。');
    }
    return await response.json();
  } catch (error) {
    console.error('エラー発生:', error.message);
    // ユーザーに表示するメッセージ
    return 'データの取得に失敗しました。もう一度お試しください。';
  }
};

このコードでは、ユーザーに対して詳細な技術的情報ではなく、シンプルで理解しやすいメッセージを提供しています。

3. 共通のエラーハンドリング関数を作成する

アプリケーション内で同様のエラーハンドリング処理が複数の場所で必要な場合、共通のエラーハンドリング関数を作成して使い回すことで、コードの重複を防ぎ、メンテナンス性を高めることができます。

const handleError = (error: Error) => {
  console.error('共通エラーハンドリング:', error.message);
  return 'エラーが発生しました。再試行してください。';
};

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('データ取得に失敗しました');
    }
    return await response.json();
  } catch (error) {
    return handleError(error);
  }
};

このように、共通のエラーハンドリング関数を作成することで、エラーハンドリングのロジックを一箇所に集約し、再利用可能な形で管理できます。

4. 特定のエラーパターンに対応する

非同期処理では、ネットワークエラーやタイムアウトなど、さまざまなエラーパターンが存在します。これらのエラーパターンに応じて、適切な対処方法を用意することが重要です。

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('リソースが見つかりません');
      } else if (response.status === 500) {
        throw new Error('サーバーエラーが発生しました');
      }
    }
    return await response.json();
  } catch (error) {
    console.error('エラー発生:', error.message);
    return 'データの取得に失敗しました。後ほど再試行してください。';
  }
};

この例では、HTTPステータスコードに基づいて異なるエラーメッセージを出力しています。エラーの内容に応じた適切な処理を行うことで、ユーザーにより具体的な情報を提供することができます。

5. エラーを再スローする

エラーをキャッチした後、さらに上位の処理に委譲する必要がある場合、エラーを再スローすることが効果的です。これにより、上位層の関数でエラーの管理や報告ができ、システム全体で一貫したエラーハンドリングが可能になります。

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('データ取得に失敗しました');
    }
    return await response.json();
  } catch (error) {
    console.error('データ取得エラー:', error.message);
    throw error; // エラーを再スローする
  }
};

const mainProcess = async () => {
  try {
    const data = await fetchData();
    console.log('データ:', data);
  } catch (error) {
    console.error('メイン処理でエラーが発生:', error.message);
  }
};

mainProcess();

この例では、fetchData関数内でキャッチされたエラーが再スローされ、上位のmainProcess関数で最終的に処理されています。

6. TypeScriptの型システムを活用する

TypeScriptを使っている場合、型システムを活用して、エラーの種類やエラーハンドリングのロジックを明示的に定義することができます。これにより、エラーハンドリングがより厳密かつ安全に行えるようになります。

次に、TypeScriptの型システムを活用したエラーハンドリングについて説明します。

TypeScriptの型システムを活用したエラーハンドリング

TypeScriptでは、静的型付けによってエラーハンドリングがより安全かつ効率的に行えます。型システムを適切に活用することで、非同期処理におけるエラーの種類やエラーハンドリングのロジックを明示的に定義し、ランタイムエラーのリスクを減らすことができます。また、型を活用することで、エラーの発生可能性をコンパイル時に検知しやすくなり、コードの安全性が向上します。

1. エラー型の定義

TypeScriptでは、エラーオブジェクトをより具体的に定義することができます。カスタムのエラー型を使用して、特定のエラーに対するハンドリングを明確にすることで、予期しないエラーに対する対処が容易になります。

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

上記の例では、NetworkErrorValidationErrorという2種類のカスタムエラーを定義しています。このようにエラー型を定義することで、エラーの内容に応じた適切なハンドリングが可能になります。

2. エラーハンドリングにおける型アノテーションの活用

非同期処理の戻り値に対して、型アノテーションを用いることで、エラーが発生する可能性のある関数を安全に扱うことができます。これにより、エラーが発生する場合と成功した場合を型として明示することで、コードの可読性と安全性が向上します。

const fetchData = async (): Promise<string | Error> => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new NetworkError('データ取得に失敗しました');
    }
    return await response.json();
  } catch (error) {
    return error instanceof Error ? error : new Error('不明なエラー');
  }
};

このコードでは、fetchData関数の戻り値がstringまたはError型であることを明示しています。これにより、呼び出し側で戻り値の型に基づいた適切な処理を行うことが可能です。

3. カスタム型を使ったエラーハンドリング

TypeScriptでは、より厳密にエラーハンドリングを行うために、カスタム型を使用してエラーの内容を定義することができます。これにより、特定のエラーパターンに応じた柔軟なエラーハンドリングが可能になります。

type APIResponse<T> = {
  success: boolean;
  data?: T;
  error?: string;
};

const fetchAPIData = async (): Promise<APIResponse<string>> => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      return { success: false, error: 'データ取得に失敗しました' };
    }
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: error instanceof Error ? error.message : '不明なエラー' };
  }
};

この例では、APIResponse型を使用して、データ取得に成功した場合と失敗した場合の構造を明示的に定義しています。これにより、エラーと成功時のレスポンスを区別しやすくなり、コードの信頼性が向上します。

4. エラーの型ガードを利用する

TypeScriptの型ガード機能を利用することで、特定のエラーが発生した際に、そのエラーの型をチェックし、適切な処理を行うことができます。これにより、異なるエラータイプに応じた処理を分岐させることが容易になります。

const isNetworkError = (error: unknown): error is NetworkError => {
  return error instanceof NetworkError;
};

const processAPIData = async () => {
  try {
    const data = await fetchAPIData();
    if (!data.success) {
      throw new Error(data.error);
    }
    console.log('データ:', data.data);
  } catch (error) {
    if (isNetworkError(error)) {
      console.error('ネットワークエラー:', error.message);
    } else {
      console.error('その他のエラー:', error instanceof Error ? error.message : '不明なエラー');
    }
  }
};

processAPIData();

この例では、isNetworkErrorという型ガード関数を使用して、特定のエラー型に基づいたハンドリングを行っています。型ガードを使うことで、エラーの種類に応じた適切な処理が可能になります。

5. エラーオブジェクトに追加情報を付加する

エラーの詳細情報を追跡するために、エラーオブジェクトにカスタムフィールドを追加して、エラーログやデバッグの際に役立つ情報を持たせることができます。これにより、問題の発生源を特定しやすくなります。

class CustomError extends Error {
  timestamp: Date;
  context: string;

  constructor(message: string, context: string) {
    super(message);
    this.timestamp = new Date();
    this.context = context;
  }
}

const fetchDataWithContext = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new CustomError('データ取得に失敗しました', 'fetchDataWithContext');
    }
    return await response.json();
  } catch (error) {
    if (error instanceof CustomError) {
      console.error(`エラー: ${error.message}, コンテキスト: ${error.context}, 発生時刻: ${error.timestamp}`);
    } else {
      console.error('一般的なエラー:', error instanceof Error ? error.message : '不明なエラー');
    }
  }
};

fetchDataWithContext();

この例では、エラーオブジェクトに発生時刻と処理のコンテキスト情報を付加しています。これにより、エラーハンドリングがより具体的かつ有用なものとなり、デバッグが容易になります。

次に、非同期処理のエラーハンドリングに関する演習問題を紹介します。

非同期処理のエラーハンドリングに関する演習問題

TypeScriptでの非同期処理におけるエラーハンドリングの理解を深めるため、ここではいくつかの演習問題を通じて実践的なスキルを磨きます。これらの問題は、Promiseやasync/awaitを使用したエラーハンドリングのベストプラクティスや、型システムを活用したアプローチを確認するのに役立ちます。

演習1: 基本的なPromiseのエラーハンドリング

以下のコードには、Promiseを使った非同期処理があります。しかし、エラーハンドリングが不足しています。このコードにエラーハンドリングを追加してください。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        resolve('データ取得成功');
      } else {
        reject('データ取得失敗');
      }
    }, 1000);
  });
};

fetchData()
  .then((data) => {
    console.log(data);
  });

問題:

  • エラーが発生した場合でもアプリケーションがクラッシュしないようにするために、適切なエラーハンドリングを追加してください。

ヒント:

  • catchメソッドを使用して、エラーを捕捉し、処理してください。

演習2: async/awaitとtry/catchのエラーハンドリング

次のasync/awaitを使用したコードには、エラーハンドリングが欠けています。このコードにtry/catchを追加して、エラーハンドリングを実装してください。

const fetchDataAsync = async () => {
  const success = Math.random() > 0.5;
  if (success) {
    return 'データ取得成功';
  } else {
    throw new Error('データ取得に失敗しました');
  }
};

const processAsync = async () => {
  const data = await fetchDataAsync();
  console.log('データ:', data);
};

processAsync();

問題:

  • processAsync関数にエラーハンドリングを追加して、エラーが発生した場合でも正しく対応できるようにしてください。

ヒント:

  • try/catch構文を使ってエラーを捕捉し、エラーが発生した場合にはconsole.errorでエラーメッセージを出力してください。

演習3: 型を活用したエラーハンドリング

次のコードでは、非同期処理の成功と失敗の両方を型で管理しています。このfetchAPIData関数を使って、エラーハンドリングを行う新しい関数を作成してください。

type APIResponse<T> = {
  success: boolean;
  data?: T;
  error?: string;
};

const fetchAPIData = async (): Promise<APIResponse<string>> => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      return { success: false, error: 'データ取得に失敗しました' };
    }
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: error instanceof Error ? error.message : '不明なエラー' };
  }
};

問題:

  • fetchAPIData関数を呼び出し、成功した場合はデータをログに出力し、失敗した場合はエラーメッセージをログに出力する関数を作成してください。

ヒント:

  • successフラグに基づいて、成功時の処理とエラー処理を分岐させてください。

演習4: カスタムエラークラスの作成

以下のコードでは、特定のエラー(ネットワークエラーとバリデーションエラー)に対する処理を実装する必要があります。カスタムエラークラスを作成し、エラーハンドリングを強化してください。

const fetchDataWithCustomError = async () => {
  const success = Math.random() > 0.5;
  if (!success) {
    // ここに適切なエラーをスローする処理を追加
    throw new Error('データ取得に失敗しました');
  }
  return 'データ取得成功';
};

const processData = async () => {
  try {
    const data = await fetchDataWithCustomError();
    console.log('データ:', data);
  } catch (error) {
    // カスタムエラーに応じて処理を分岐
    console.error('エラー:', error.message);
  }
};

processData();

問題:

  • NetworkErrorValidationErrorのカスタムエラークラスを定義し、それぞれに応じた処理をprocessData関数に追加してください。

ヒント:

  • instanceofを使って、エラーの種類に応じた処理を実装してください。

これらの演習を通じて、非同期処理におけるエラーハンドリングのスキルを向上させ、実践的なTypeScriptの使い方をマスターしてください。次に、この記事のまとめを行います。

まとめ

本記事では、TypeScriptにおけるPromiseとasync/awaitを使った非同期処理のエラーハンドリングについて詳しく解説しました。Promiseのcatchメソッドによる基本的なエラーハンドリングから、async/awaitでのtry/catch構文、さらにTypeScriptの型システムを活用した高度なエラーハンドリング手法まで、多角的なアプローチを紹介しました。リソースのクリーンアップに役立つfinallyの使い方や、カスタムエラークラスを作成して特定のエラーを明示的に処理する方法も学びました。

これらのベストプラクティスを活用して、非同期処理のエラーに対する適切な対応を行い、安定性の高いアプリケーションを構築しましょう。

コメント

コメントする

目次
  1. Promiseによるエラーハンドリング
    1. Promiseの基本構造
    2. Promiseのエラーハンドリング方法
  2. async/awaitによるエラーハンドリング
    1. async/awaitの基本構造
    2. async/awaitのエラーハンドリング方法
    3. Promiseとの比較
  3. 非同期処理におけるエラーハンドリングの課題
    1. Promiseチェーンにおけるエラーの課題
    2. async/awaitにおけるエラーの課題
    3. 非同期処理の失敗が見落とされる問題
  4. Promiseのcatchメソッドを活用したパターン
    1. catchメソッドの基本的な使い方
    2. 中間のPromiseでのエラーハンドリング
    3. 複数のcatchを使ったエラーハンドリング
    4. catchの再スローによるエラーチェーンの構築
  5. async/awaitとtry/catchの応用例
    1. 複数の非同期処理のエラーハンドリング
    2. 並行処理のエラーハンドリング
    3. 特定の非同期処理だけエラーハンドリングするパターン
    4. async/awaitによる再スローの活用
  6. 非同期関数のエラーを一括管理する方法
    1. エラーハンドリング用のラッパー関数を作成
    2. カスタムエラーハンドラーの導入
    3. エラーを含む結果を返すパターン
  7. finallyによるリソースのクリーンアップ
    1. Promiseにおけるfinallyの使用例
    2. async/awaitにおけるfinallyの使用例
    3. finallyの典型的な使用シナリオ
  8. エラーハンドリングのベストプラクティス
    1. 1. すべての非同期処理にエラーハンドリングを実装する
    2. 2. エラーをユーザーフレンドリーにする
    3. 3. 共通のエラーハンドリング関数を作成する
    4. 4. 特定のエラーパターンに対応する
    5. 5. エラーを再スローする
    6. 6. TypeScriptの型システムを活用する
  9. TypeScriptの型システムを活用したエラーハンドリング
    1. 1. エラー型の定義
    2. 2. エラーハンドリングにおける型アノテーションの活用
    3. 3. カスタム型を使ったエラーハンドリング
    4. 4. エラーの型ガードを利用する
    5. 5. エラーオブジェクトに追加情報を付加する
  10. 非同期処理のエラーハンドリングに関する演習問題
    1. 演習1: 基本的なPromiseのエラーハンドリング
    2. 演習2: async/awaitとtry/catchのエラーハンドリング
    3. 演習3: 型を活用したエラーハンドリング
    4. 演習4: カスタムエラークラスの作成
  11. まとめ