Fetch APIで非同期通信をマスターする——GET・POST・エラーハンドリング・React連携まで

JavaScriptのFetch APIの基本から、POST送信・エラーハンドリング・タイムアウト・ReactのuseEffectとの組み合わせまで実務で使うパターンをコード例で整理する。

XMLHttpRequestの時代は終わり、今はFetch APIが非同期通信の標準だ。

async/awaitと組み合わせれば直感的に書けるが、エラーハンドリングやタイムアウトなど、知っておかないとハマるポイントがある。実務でよく使うパターンをまとめる。


基本のGETリクエスト

// シンプルなGET
const res = await fetch('https://api.example.com/users');
const data = await res.json();
console.log(data);

注意点: fetchはHTTPエラー(404・500など)でもPromiserejectしない。res.okで確認が必要だ。

// ❌ これだとHTTPエラーを検知できない
const res = await fetch('/api/users');
const data = await res.json(); // 404でもここが実行される

// ✅ res.ok を確認する
const res = await fetch('/api/users');
if (!res.ok) {
  throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();

汎用のfetchラッパーを作る

毎回エラーチェックを書くのは冗長なので、ラッパー関数を作るのが実務では定石だ。

class HttpError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

async function apiFetch<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const res = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
    ...options,
  });

  if (!res.ok) {
    const message = await res.text().catch(() => `HTTP ${res.status}`);
    throw new HttpError(res.status, message);
  }

  return res.json() as Promise<T>;
}

// 使い方
type User = { id: number; name: string };
const user = await apiFetch<User>('/api/users/1');

POSTリクエスト

type CreateUserInput = { name: string; email: string };
type User = { id: number; name: string; email: string };

async function createUser(input: CreateUserInput): Promise<User> {
  return apiFetch<User>('/api/users', {
    method: 'POST',
    body: JSON.stringify(input),
  });
}

認証ヘッダーの付与

// JWTトークンをAuthorizationヘッダーに含める
async function authenticatedFetch<T>(url: string, options?: RequestInit): Promise<T> {
  const token = localStorage.getItem('access_token');
  return apiFetch<T>(url, {
    ...options,
    headers: {
      Authorization: `Bearer ${token}`,
      ...options?.headers,
    },
  });
}

タイムアウトの実装

Fetch APIにはタイムアウト機能が標準でないため、AbortControllerで実装する。

async function fetchWithTimeout<T>(
  url: string,
  timeoutMs: number = 10000,
  options?: RequestInit
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await apiFetch<T>(url, {
      ...options,
      signal: controller.signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

ReactのuseEffectと組み合わせる

type Post = { id: number; title: string; body: string };

function PostDetail({ postId }: { postId: number }) {
  const [post, setPost] = useState<Post | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false; // クリーンアップ時の二重実行を防ぐ

    async function loadPost() {
      try {
        setIsLoading(true);
        setError(null);
        const data = await apiFetch<Post>(`/api/posts/${postId}`);
        if (!cancelled) setPost(data);
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'エラーが発生しました');
        }
      } finally {
        if (!cancelled) setIsLoading(false);
      }
    }

    loadPost();
    return () => { cancelled = true; }; // クリーンアップ
  }, [postId]);

  if (isLoading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  if (!post) return null;

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

Next.js App Router でのfetch

App Routerではfetchがキャッシュ拡張されており、Server Components内でそのまま使える。

// app/users/page.tsx(Server Component)
export default async function UsersPage() {
  // デフォルトでキャッシュされる
  const users = await fetch('https://api.example.com/users').then(r => r.json());

  // キャッシュしない(毎回フェッチ)
  const liveData = await fetch('/api/realtime', { cache: 'no-store' }).then(r => r.json());

  // 一定時間でrevalidate
  const news = await fetch('/api/news', { next: { revalidate: 60 } }).then(r => r.json());

  return <div>{/* ... */}</div>;
}

まとめ

パターンポイント
基本GETres.okチェックを忘れない
POSTbody: JSON.stringify()でシリアライズ
エラーハンドリングラッパー関数で共通化する
タイムアウトAbortControllerで実装
React連携cancelledフラグでアンマウント後の更新を防ぐ
Next.js App Routercacheオプションでキャッシュ戦略を指定

Fetch APIの基礎はJavaScript本格入門の10章で丁寧に解説されている。