TypeScriptで学ぶ!非同期処理のエラーハンドリングと型安全な実装法

TypeScriptは、静的型付けによるコードの信頼性向上を目的とした人気のプログラミング言語です。JavaScriptを拡張しているため、JavaScriptのすべての機能を利用しながら、さらに型安全性を付加できます。本記事では、特に非同期処理に焦点を当て、TypeScriptの強みを活かしたエラーハンドリングと型安全なコードの実装方法について解説します。非同期処理は、サーバーからのデータ取得や外部APIとの通信など、多くの開発シナリオで不可欠です。ここでは、非同期処理に潜むリスクやその解決策について学び、堅牢なプログラムを作成するための技術を習得しましょう。

目次

非同期処理の基本とTypeScriptの特徴

非同期処理は、プログラムが他のタスクの完了を待たずに別の処理を続行できる仕組みです。これにより、効率的なデータ処理や、UIの応答性を維持しながら外部リソースとの通信が可能になります。JavaScriptでは、非同期処理を行うために主にPromiseasync/awaitが使用され、これにより非同期タスクの開始と終了を管理できます。

TypeScriptにおける型安全性の役割

TypeScriptは、JavaScriptの非同期処理をさらに強化するために、静的型付けを導入しています。これにより、コンパイル時にエラーを発見しやすくなり、予期しないバグやエラーを防ぐことができます。非同期処理に型を適用することで、APIから返されるデータの形式や、エラーハンドリングの際に予想される戻り値を厳密に定義でき、信頼性の高いコードを書くことが可能です。

例えば、非同期関数の戻り値がPromise<number>である場合、その関数は必ずnumberを返すことが期待されるため、誤ったデータ型の処理を未然に防げます。

エラーハンドリングの一般的な方法

非同期処理を行う際、予期しないエラーが発生する可能性は避けられません。そのため、適切なエラーハンドリングは非常に重要です。JavaScriptおよびTypeScriptでは、主にtry-catchPromiseを使ったエラーハンドリングが一般的です。

try-catch構文によるエラーハンドリング

async/awaitを使用する非同期処理では、try-catch構文がエラーハンドリングに頻繁に用いられます。tryブロック内で非同期処理を行い、エラーが発生した場合はcatchブロックで処理します。

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

上記の例では、fetch関数が失敗した場合、catchブロックでエラーが捕捉され、カスタムメッセージが出力されます。これにより、エラーの場所と内容が明確に記録され、デバッグが容易になります。

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

非同期処理を行う場合、Promisethencatchメソッドを使用してエラーハンドリングすることも一般的です。thenメソッドで非同期処理の結果を取得し、catchでエラーを処理します。

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error fetching data:', error));

Promiseを直接利用する場合も、エラーをキャッチして適切に処理することが重要です。どちらの方法も、非同期処理の流れにおける例外を捕捉し、アプリケーションのクラッシュを防ぎます。

型安全な非同期処理の実装法

TypeScriptでは、非同期処理における型の適用により、エラーやデータの不整合を未然に防ぐことができます。Promiseasync/awaitの構文を使用する際、返り値やエラーの型を明示することで、信頼性の高いコードを実現します。

Promiseの型指定

TypeScriptでPromiseを使用する際、返されるデータの型を指定することで、型安全性を保つことができます。例えば、APIから数値データが返されることがわかっている場合、Promise<number>と指定することで、コンパイル時に型チェックを行い、予期しない型のエラーを防ぎます。

function getNumber(): Promise<number> {
  return new Promise((resolve, reject) => {
    resolve(42);
  });
}

getNumber().then((result) => {
  console.log(result);  // コンパイル時にnumber型が保証される
});

このように、関数の戻り値にPromise<number>という型を明示することで、thenブロック内で常にnumber型のデータを扱うことが保証されます。

async/await構文の型安全な利用

async/await構文もTypeScriptでは広く使用され、より直感的な非同期処理の実装を可能にします。async関数では、戻り値に型を明示的に指定することで、処理の結果がどのような型になるかを正確に表現できます。

async function fetchData(): Promise<string> {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data.message;  // 型がstringであることが保証される
}

この例では、fetchData関数の戻り値がPromise<string>であると指定されているため、データが文字列として扱われることが保証され、コンパイル時にエラーを未然に防ぐことができます。

エラーハンドリングにおける型の重要性

try-catch構文と併用する際も、型を明示することで、エラーが発生した際に想定外の動作を防ぎます。エラーオブジェクトの型を明示的に指定しておくことで、エラーメッセージや原因の取得も型安全に行えます。

async function fetchDataSafe(): Promise<void> {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error: any) {
    console.error('Error:', error.message);  // 型安全なエラーハンドリング
  }
}

このように、非同期処理において型を適切に指定することで、コードの安全性と可読性が向上し、信頼性の高いアプリケーションの開発が可能になります。

非同期関数の型定義とReturnTypeの利用

TypeScriptの強力な機能の一つに、関数の戻り値の型を定義して、型安全なコードを書くことが挙げられます。非同期関数でも、正確な型を使うことで、関数の返り値が予期した通りのデータ型であることを保証できます。ここでは、非同期関数の型定義と、それをさらに活用するためのReturnTypeユーティリティ型の使い方を解説します。

非同期関数の型定義

TypeScriptでは、非同期関数の戻り値が必ずPromise型であるため、戻り値の型を正確に定義することが重要です。例えば、Promise<string>のように型を指定することで、関数が必ず文字列を返すPromiseを返すことが保証されます。

async function fetchUserName(userId: number): Promise<string> {
  const response = await fetch(`https://api.example.com/user/${userId}`);
  const data = await response.json();
  return data.name;  // 返り値の型がstringであることを保証
}

この例では、fetchUserName関数がユーザーIDを引数として受け取り、ユーザー名(string)を返す非同期関数です。戻り値がPromise<string>で型定義されているため、後続の処理で型の一貫性が保たれます。

ReturnTypeユーティリティ型の活用

ReturnTypeは、既存の関数からその戻り値の型を抽出するためのTypeScriptのユーティリティ型です。これにより、コードの再利用性を高めつつ、型定義を簡潔に保つことができます。非同期関数でも、戻り値の型を自動的に取得して利用することが可能です。

type UserNameReturnType = ReturnType<typeof fetchUserName>;

async function logUserName(userId: number): Promise<void> {
  const userName: UserNameReturnType = await fetchUserName(userId);
  console.log(userName);
}

上記のコードでは、ReturnType<typeof fetchUserName>を使ってfetchUserName関数の戻り値の型を取得し、それを他の関数で利用しています。これにより、型定義の重複を避け、コードの一貫性を保ちながら型安全な実装ができます。

ReturnTypeを使ったエラーハンドリング

ReturnTypeは、エラーハンドリングでも役立ちます。非同期関数が複数の異なる型を返す場合でも、ReturnTypeを使用することで、一貫した型定義を行い、エラー処理を型安全に行うことができます。

async function fetchData(): Promise<{ message: string } | null> {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return { message: data.message };
  } catch (error) {
    console.error('Error fetching data:', error);
    return null;
  }
}

type FetchDataReturnType = ReturnType<typeof fetchData>;

async function handleData(): Promise<void> {
  const result: FetchDataReturnType = await fetchData();
  if (result) {
    console.log(result.message);
  } else {
    console.log('No data available');
  }
}

このように、ReturnTypeを使用することで、非同期関数の型定義を効率的に管理し、型安全なコードを実現できます。

非同期エラーの型安全な管理

非同期処理におけるエラーハンドリングは、コードの安定性と信頼性を高めるために不可欠です。TypeScriptを利用することで、エラーの型を厳密に管理し、型安全なエラーハンドリングが可能になります。これにより、予期しないエラーの発生を防ぎ、エラー処理が一貫して適切に行われるようになります。

カスタムエラークラスを使った型安全なエラーハンドリング

TypeScriptでは、カスタムエラークラスを作成することで、特定のエラーパターンを明確にし、型安全にエラーを管理することができます。これにより、エラーの種類に応じた適切な処理が可能です。

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

async function fetchData(): Promise<string> {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new FetchError('Failed to fetch data');
    }
    const data = await response.json();
    return data.message;
  } catch (error) {
    if (error instanceof FetchError) {
      console.error('Custom Fetch Error:', error.message);
    } else {
      console.error('Unknown Error:', error);
    }
    throw error;
  }
}

この例では、FetchErrorというカスタムエラークラスを定義し、非同期処理で発生する特定のエラーを型安全に扱っています。エラーが発生した際、error instanceof FetchErrorを使ってエラーの種類を確認し、それに応じた処理を行います。

エラーオブジェクトの型定義

非同期処理で発生するエラーは、標準のErrorオブジェクトだけでなく、APIから返されるカスタムエラーデータである場合もあります。これらのエラーデータに対して型を明示的に定義し、型安全にエラーハンドリングを行うことが重要です。

interface ApiError {
  statusCode: number;
  message: string;
}

async function fetchDataWithError(): Promise<string> {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      const errorData: ApiError = await response.json();
      throw new Error(`Error ${errorData.statusCode}: ${errorData.message}`);
    }
    const data = await response.json();
    return data.message;
  } catch (error) {
    if (error instanceof Error) {
      console.error('API Error:', error.message);
    } else {
      console.error('Unknown Error:', error);
    }
    throw error;
  }
}

この例では、ApiErrorインターフェースを使用してAPIエラーの型を定義し、エラーが発生した際にその型に従ってエラーメッセージを処理しています。エラーのデータ構造を事前に定義することで、エラーハンドリングがより安全で信頼性の高いものになります。

非同期エラーの再スローと型保証

エラーを捕捉した後、そのエラーを再スローして他の関数や呼び出し元で処理させることもよくあります。TypeScriptでは、再スローされるエラーも型を保証することで、エラー処理の一貫性を維持できます。

async function processData(): Promise<void> {
  try {
    const message = await fetchData();
    console.log('Fetched message:', message);
  } catch (error) {
    console.error('Error occurred during process:', error);
    // 必要に応じて再スロー
    throw error;
  }
}

processData().catch((error) => {
  console.error('Error handled at top level:', error);
});

このように、エラーをキャッチして再スローする場合も、型を明示的にすることで、処理の一貫性を保ちながら、予期しないエラーの混入を防ぎます。

非同期エラーの型安全な管理を行うことで、エラー処理が明確になり、デバッグが容易になるだけでなく、コード全体の信頼性が向上します。

`Promise.all`でのエラーハンドリング

Promise.allは、複数の非同期処理を同時に実行し、その結果を一括して処理するために便利な手法です。しかし、すべてのPromiseが成功する必要があり、1つでも失敗すると全体が失敗してしまうため、エラーハンドリングが非常に重要です。ここでは、Promise.allを使った非同期処理と、エラー発生時の型安全な対処法について説明します。

`Promise.all`の基本的な使い方

Promise.allは、複数のPromiseを受け取り、それらがすべて解決されたときに、その結果を配列として返します。非同期処理が並列に行われるため、効率的に複数のリクエストを処理できます。

async function fetchMultipleData() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3',
  ];

  try {
    const [data1, data2, data3] = await Promise.all(
      urls.map((url) => fetch(url).then((res) => res.json()))
    );
    console.log('All data fetched:', data1, data2, data3);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

この例では、Promise.allが各URLからのデータを並行して取得します。すべてのPromiseが成功すれば結果が配列で返されますが、どれか一つでも失敗すると、全体がエラーとして処理されます。

`Promise.all`のエラー処理

Promise.allでエラーが発生した場合、すべての処理が一度に失敗します。これにより、失敗したリクエストがどれであるかが不明になるため、エラー処理の際には注意が必要です。個々のPromiseのエラーハンドリングを行うことで、エラーの原因を明確にできます。

async function fetchMultipleDataWithErrorHandling() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3',
  ];

  const promises = urls.map(async (url) => {
    try {
      const response = await fetch(url);
      return await response.json();
    } catch (error) {
      console.error(`Error fetching data from ${url}:`, error);
      return null;  // エラーが発生した場合、nullを返す
    }
  });

  const results = await Promise.all(promises);
  console.log('Results:', results);
}

このコードでは、各Promise内で個別にエラーハンドリングを行い、エラーが発生しても他のPromiseの結果を影響なく取得できるようにしています。失敗したリクエストはnullとして処理されるため、後続の処理でそれを確認し、適切な対応が可能です。

エラーが発生した処理だけを特定する方法

エラーハンドリングをさらに進化させ、失敗したPromiseのみを特定したい場合は、Promise.allSettledを利用します。Promise.allSettledは、すべてのPromiseが完了するまで待機し、それぞれの成功や失敗の結果を確認することができます。

async function fetchWithAllSettled() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3',
  ];

  const results = await Promise.allSettled(
    urls.map((url) => fetch(url).then((res) => res.json()))
  );

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Data ${index + 1}:`, result.value);
    } else {
      console.error(`Failed to fetch data ${index + 1}:`, result.reason);
    }
  });
}

Promise.allSettledは、fulfilled(成功)かrejected(失敗)かを確認できるため、エラーが発生したリクエストを特定し、他の成功した結果に影響を与えずに処理できます。

まとめ

Promise.allは効率的な非同期処理に非常に便利ですが、エラーハンドリングが重要です。Promise.allSettledを使用することで、エラー発生時にも他の結果に影響を与えず、型安全にエラーハンドリングが可能です。

外部ライブラリを活用した非同期処理の改善

TypeScriptでは、標準的なfetchPromiseによる非同期処理をサポートしていますが、外部ライブラリを利用することで、非同期処理の効率化やエラーハンドリングの改善が可能です。特に、axiosなどのライブラリは、使いやすく型安全な非同期処理を提供し、コードの読みやすさや保守性を向上させます。ここでは、外部ライブラリを活用した非同期処理の具体例を紹介します。

axiosの基本的な使い方

axiosは、HTTPリクエストを簡潔に行うための人気ライブラリで、PromiseベースのAPIを提供しています。TypeScriptと組み合わせることで、リクエストやレスポンスの型を安全に扱うことができます。

import axios from 'axios';

interface ApiResponse {
  id: number;
  name: string;
}

async function fetchUser(userId: number): Promise<ApiResponse> {
  try {
    const response = await axios.get<ApiResponse>(`https://api.example.com/user/${userId}`);
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.error('Axios error:', error.message);
    } else {
      console.error('Unexpected error:', error);
    }
    throw error;
  }
}

fetchUser(1).then((data) => console.log(data));

この例では、axios.getメソッドを使用してAPIからデータを取得し、レスポンスの型をApiResponseとして定義しています。これにより、返されるデータの構造が厳密に管理され、予期しない型のエラーを防ぐことができます。

エラーハンドリングの改善

axiosは標準でエラーハンドリングをサポートしており、エラーが発生した場合に詳細な情報を取得できます。TypeScriptの型チェックを活用することで、エラーハンドリングをより精密に行うことが可能です。

async function fetchDataWithErrorHandling(url: string): Promise<void> {
  try {
    const response = await axios.get(url);
    console.log('Data:', response.data);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.error('Axios Error: ', error.response?.status, error.response?.data);
    } else {
      console.error('Unexpected Error:', error);
    }
  }
}

fetchDataWithErrorHandling('https://api.example.com/data');

この例では、axios.isAxiosErrorを使ってaxios特有のエラーかどうかを判定し、HTTPステータスコードやエラーメッセージを型安全に処理しています。

axiosによるリクエストの設定とインターセプター

axiosは、リクエストやレスポンスをカスタマイズするためのインターセプター機能を提供しています。これにより、全てのリクエストやレスポンスに対して共通の処理を挟むことができ、認証トークンの追加やエラーハンドリングの共通化が可能です。

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 1000,
});

apiClient.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      console.error('Unauthorized access - please log in again.');
    }
    return Promise.reject(error);
  }
);

このコードでは、axios.createを使ってカスタムHTTPクライアントを作成し、インターセプターを使ってリクエストのヘッダーに認証トークンを自動的に追加しています。また、レスポンスが401(認証エラー)の場合に特定のエラーハンドリングを行う例も示しています。

外部ライブラリの活用による効率化のメリット

外部ライブラリを使うことで、次のようなメリットが得られます:

  • 型安全性の向上:APIリクエストやレスポンスの型を厳密に定義できるため、予期しないエラーを防げる。
  • コードの簡潔化axiosの簡潔な構文により、fetchを使った場合に比べてコードが短く、読みやすくなる。
  • 高度なカスタマイズ:インターセプターなどの機能を使うことで、共通処理をリクエストやレスポンスに組み込み、開発効率が向上する。

このように、axiosのような外部ライブラリを使うことで、非同期処理の実装がより効率的で型安全に行えるようになります。

非同期処理におけるユニットテストの重要性

非同期処理は、多くのアプリケーションにおいてデータの取得や外部APIとの通信など、不可欠な要素となっています。しかし、非同期処理が絡む場合、テストが複雑になることがあります。TypeScriptでは、型安全なコードの実装だけでなく、非同期処理の動作を保証するために、ユニットテストを活用することが重要です。ここでは、非同期処理のテスト方法と、その重要性について解説します。

非同期処理をテストする際の課題

非同期処理におけるテストの課題は、実行が即時に完了せず、処理結果が時間差で得られる点にあります。これにより、テストケースがどのように動作するかを正確に制御する必要があります。さらに、APIリクエストやデータベースアクセスなどの外部リソースに依存する処理を直接テストする場合、外部システムの状態に依存してしまう可能性があるため、テストの再現性を保つことが困難になる場合があります。

Jestを使った非同期処理のユニットテスト

TypeScriptの非同期処理をテストする際には、Jestなどのテストフレームワークが広く利用されています。Jestは、async/awaitPromiseをサポートしており、非同期処理をテストするためのさまざまなツールを提供しています。

import axios from 'axios';
import { fetchUser } from './userService'; // ユーザー情報を取得する非同期関数

jest.mock('axios');

test('fetchUser returns user data', async () => {
  const mockData = { id: 1, name: 'John Doe' };
  axios.get.mockResolvedValue({ data: mockData });

  const result = await fetchUser(1);

  expect(result).toEqual(mockData);
});

このテストでは、axiosをモックして非同期関数fetchUserの動作を確認しています。モックを使用することで、実際に外部APIを呼び出すことなく、関数の動作だけをテストすることができます。これにより、外部要因によるテスト結果の変動を防ぎ、安定したテストを実現します。

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

非同期処理では、エラーハンドリングも重要な要素となるため、エラー発生時の挙動をテストすることが必要です。Jestでは、エラーを期待するテストも容易に実行できます。

test('fetchUser throws an error if API call fails', async () => {
  axios.get.mockRejectedValue(new Error('API Error'));

  await expect(fetchUser(1)).rejects.toThrow('API Error');
});

この例では、API呼び出しが失敗した場合に、fetchUser関数が適切なエラーをスローすることをテストしています。rejects.toThrowを使うことで、非同期関数がエラーを投げることを確認できます。

非同期処理におけるテストのベストプラクティス

非同期処理のテストを効率的に行うためのベストプラクティスとして、以下の点が挙げられます。

1. モックを利用した外部依存の排除

非同期処理でAPI呼び出しやデータベースアクセスが含まれる場合、外部リソースへの依存を最小限に抑えるために、モックを活用します。これにより、テストが外部環境に依存せず、確実な結果が得られます。

2. エラーハンドリングの確認

エラーハンドリングが正しく行われていることを確認するため、期待されるエラーが発生した際の挙動を必ずテストします。これにより、予期せぬエラーが発生した際の処理を信頼性の高いものにします。

3. 時間依存の非同期処理のテスト

非同期処理の中には、時間の経過に依存するもの(タイムアウトや待機時間を含む処理)があります。このような場合、JestのsetTimeoutjest.advanceTimersByTimeなどを使用して、時間をシミュレートすることで、予期せぬタイミングでエラーが発生することを防ぎます。

jest.useFakeTimers();

test('waits 1 second before resolving', () => {
  const mockCallback = jest.fn();

  setTimeout(mockCallback, 1000);

  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
});

このテストでは、タイマーを操作して非同期処理が時間通りに実行されるかを確認しています。

まとめ

非同期処理は、予期しないエラーや時間的な不確定性を伴うため、ユニットテストが非常に重要です。TypeScriptとJestを活用することで、非同期処理の挙動やエラーハンドリングを確実にテストし、信頼性の高いアプリケーションを開発することができます。

具体的な応用例: REST APIとの連携

TypeScriptを使った非同期処理の実際の応用例として、REST APIとの連携は非常に重要です。多くのアプリケーションでは、外部サービスやデータベースとの通信を行うために、REST APIを利用しています。TypeScriptでAPIと連携する際には、型安全性を保ちながら非同期リクエストを効率的に扱うことができます。ここでは、具体的な実装例を紹介しながら、REST APIとの連携方法を解説します。

GETリクエストを使ったデータの取得

まず、外部APIからデータを取得する最も基本的な非同期処理であるGETリクエストの例を見てみましょう。TypeScriptを使うことで、取得したデータの型を定義し、安全にデータを操作することができます。

import axios from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(userId: number): Promise<User> {
  try {
    const response = await axios.get<User>(`https://api.example.com/users/${userId}`);
    return response.data;
  } catch (error) {
    console.error('Error fetching user:', error);
    throw error;
  }
}

fetchUser(1).then(user => console.log(user));

この例では、axios.getメソッドを使用してREST APIからユーザー情報を取得しています。レスポンスの型をUserインターフェースで定義し、データの整合性を保証しています。TypeScriptによる型定義のおかげで、APIから返されるデータが期待通りの形式であることが保証され、予期しないエラーを防ぐことができます。

POSTリクエストを使ったデータの送信

次に、POSTリクエストを使ってデータを送信する方法を見てみましょう。TypeScriptでは、送信するデータの型を明示的に定義することで、クライアントからサーバーへ送信するデータの型が厳密に管理されます。

interface NewUser {
  name: string;
  email: string;
}

async function createUser(newUser: NewUser): Promise<User> {
  try {
    const response = await axios.post<User>('https://api.example.com/users', newUser);
    return response.data;
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
}

createUser({ name: 'Jane Doe', email: 'jane@example.com' }).then(user => console.log(user));

この例では、NewUser型のオブジェクトをPOSTリクエストとしてAPIに送信しています。TypeScriptにより、送信するデータの型とAPIから返されるレスポンスの型が明確に定義されているため、データのやり取りが型安全に行われます。

PUTリクエストでのデータ更新

PUTリクエストを使用して、既存のリソースを更新する方法も紹介します。TypeScriptで型を定義することで、更新するデータやAPIから返されるデータの整合性が保たれます。

interface UpdateUser {
  name?: string;
  email?: string;
}

async function updateUser(userId: number, updatedData: UpdateUser): Promise<User> {
  try {
    const response = await axios.put<User>(`https://api.example.com/users/${userId}`, updatedData);
    return response.data;
  } catch (error) {
    console.error('Error updating user:', error);
    throw error;
  }
}

updateUser(1, { name: 'John Updated' }).then(user => console.log(user));

この例では、UpdateUser型を利用して、特定のユーザーの情報を部分的に更新しています。更新するフィールドはオプションであり、TypeScriptの型定義によってどのフィールドが必要か、どれがオプションかが明確になっています。

DELETEリクエストによるデータの削除

最後に、DELETEリクエストを使ってデータを削除する例です。リソースの削除時には、特定のデータを返すことなく、削除が成功したかどうかを確認するためにステータスコードを使用する場合があります。

async function deleteUser(userId: number): Promise<void> {
  try {
    await axios.delete(`https://api.example.com/users/${userId}`);
    console.log(`User ${userId} deleted successfully`);
  } catch (error) {
    console.error('Error deleting user:', error);
    throw error;
  }
}

deleteUser(1);

この例では、axios.deleteを使用して指定したユーザーを削除しています。削除の際、特にデータを返さない場合でも、TypeScriptを用いることでエラーハンドリングや成功したかどうかの確認を安全に行うことができます。

まとめ

REST APIとの連携は、TypeScriptを使った非同期処理の最も一般的な応用例の一つです。TypeScriptの型定義を活用することで、データの取得・送信・更新・削除が型安全に行われ、エラーハンドリングも適切に処理できます。こうした型安全性が高い実装により、バグの発生を抑え、より信頼性の高いアプリケーションを開発することが可能です。

よくあるエラーパターンとその解決法

非同期処理は便利ですが、特定のエラーパターンが発生しやすく、これらの問題に適切に対処しないと、アプリケーションの安定性が損なわれる可能性があります。ここでは、TypeScriptを用いた非同期処理でよく遭遇するエラーパターンと、それに対する型安全な解決方法を紹介します。

パターン1: 非同期処理の競合

非同期処理が同時に実行される場合、処理が競合して予期しない結果を引き起こすことがあります。例えば、ユーザーが同時に複数のデータをリクエストすると、最後に受け取ったデータだけが表示されるケースです。この問題を回避するためには、リクエストが正しい順序で処理されるように制御する必要があります。

async function fetchDataSequentially() {
  try {
    const data1 = await fetch('https://api.example.com/data1').then(res => res.json());
    const data2 = await fetch('https://api.example.com/data2').then(res => res.json());
    console.log('Data fetched in sequence:', data1, data2);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

この例では、awaitを用いることでリクエストを逐次実行し、競合を防いでいます。

パターン2: レースコンディション

複数の非同期処理が並行して実行される場合、ある処理の完了を待たずに他の処理が進行する「レースコンディション」が発生することがあります。この問題は、Promise.raceを使って一番早く完了したPromiseの結果に基づく処理を行うことで解決できます。

async function fetchFirstAvailableData() {
  try {
    const data = await Promise.race([
      fetch('https://api.example.com/data1').then(res => res.json()),
      fetch('https://api.example.com/data2').then(res => res.json()),
    ]);
    console.log('First available data:', data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

この例では、最初に成功したリクエストの結果が取得され、レースコンディションを有効に活用しています。

パターン3: エラーのキャッチ漏れ

非同期処理における一般的な問題として、Promiseasync/awaitのエラーをキャッチしない場合があります。特にPromiseのチェーンでエラーハンドリングを忘れると、例外が未処理のまま残る可能性があります。

async function fetchDataWithCatch() {
  try {
    const data = await fetch('https://api.example.com/data').then(res => res.json());
    console.log('Data:', data);
  } catch (error) {
    console.error('Error occurred:', error);
  }
}

fetchDataWithCatch();

この例では、try-catchブロックを使用してエラーを捕捉し、エラーハンドリングの抜け漏れを防いでいます。

パターン4: スワローされたエラー

エラーをキャッチしても、そのエラーを再スローしない場合、エラーが隠蔽されてしまい、後続の処理に影響を与えることがあります。エラーを適切に再スローすることで、この問題を回避できます。

async function fetchDataAndThrow() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Caught error, rethrowing:', error);
    throw error;  // エラーを再スローする
  }
}

fetchDataAndThrow().catch(error => console.error('Final catch:', error));

ここでは、catchブロック内でエラーを捕捉した後、再スローすることで、エラーが他の部分で適切に処理されるようにしています。

パターン5: 複数のエラー発生時の問題

Promise.allを使用する際、複数の非同期処理が失敗すると、どのエラーが発生したのかが不明になることがあります。この場合、Promise.allSettledを使用して各Promiseの成功・失敗状態を個別に確認することができます。

async function fetchMultipleData() {
  const results = await Promise.allSettled([
    fetch('https://api.example.com/data1').then(res => res.json()),
    fetch('https://api.example.com/data2').then(res => res.json()),
  ]);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Data ${index + 1}:`, result.value);
    } else {
      console.error(`Error in data ${index + 1}:`, result.reason);
    }
  });
}

この例では、Promise.allSettledを使って複数の非同期処理の結果を個別に確認し、エラーが発生した場合にも詳細なエラーハンドリングを行っています。

まとめ

非同期処理には多くの潜在的なエラーパターンが存在しますが、TypeScriptの型安全性を活用し、適切なエラーハンドリングと制御を行うことで、こうした問題を予防できます。各パターンに対する解決法を理解し、実践することで、非同期処理の安定性と信頼性が向上します。

まとめ

本記事では、TypeScriptを使った非同期処理におけるエラーハンドリングと型安全な実装方法について、さまざまな観点から解説しました。非同期処理の基本から、Promiseasync/awaitによるエラーハンドリング、Promise.allaxiosを利用した実践的な手法、そしてユニットテストやよくあるエラーパターンへの対処法まで、具体的な例を通じて説明しました。

非同期処理におけるエラーを適切に処理し、型安全性を保つことは、堅牢で信頼性の高いアプリケーションを開発する上で不可欠です。TypeScriptの型定義を最大限に活用することで、コードの可読性とメンテナンス性が向上し、予期せぬバグを防ぐことができます。

コメント

コメントする

目次