fetch() は現在、すべてのモダンブラウザとNode.js 18+に組み込まれています。 XMLHttpRequest を置き換え、ほとんどのプロジェクトでAxiosの必要性をなくしました。 しかし、すべてのチュートリアルが示すデフォルトの使い方 — fetch(url).then(r => r.json()) — はエラー処理を省略し、タイムアウトもなく、実際の本番環境では機能しません。 このガイドでは、実際に使えるパターンを解説します。

基本 — GETとPOST

fetch()Responseオブジェクト で解決されるPromiseを返します。GETリクエストはシンプルです:

js
const response = await fetch('https://api.example.com/products');
const products = await response.json();

JSONボディを持つPOSTリクエストには、もう少し設定が必要です:

js
const newProduct = {
  name:     'Wireless Keyboard',
  price:    79.99,
  category: 'electronics'
};

const response = await fetch('https://api.example.com/products', {
  method:  'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${authToken}`
  },
  body: JSON.stringify(newProduct)
});

const created = await response.json();
console.log(`Created product with id: ${created.id}`);
よくある間違い: POSTリクエストで Content-Type: application/json を忘れること。これがないと、多くのサーバーフレームワークはボディを解析せず、 400 Bad Requestエラーや有用なエラーメッセージのない空のリクエストボディが返ってきます。

2段階エラーチェック — response.ok

これは最も重要なパターンです。fetch() はネットワークエラー(接続なし、DNS失敗、CORSブロック)時にのみ Promiseをリジェクトします。404、401、500のレスポンスはPromiseを解決しますが、response.okfalseに設定されます。これを確認しないと、エラーレスポンスをサイレントに response.json()に渡してしまいます:

js
// ❌ Broken — 404 and 500 responses appear to succeed
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`);
  return await response.json(); // parses the error body as if it were data
}

// ✅ Correct — check response.ok before parsing
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`);

  if (!response.ok) {
    // Try to get the error message from the body if it's JSON
    const errorBody = await response.json().catch(() => null);
    const message   = errorBody?.message ?? `HTTP ${response.status}: ${response.statusText}`;
    throw new Error(message);
  }

  return await response.json();
}

response.ok はステータスコード200〜299の場合に true になります。 それ以外 — 301リダイレクト(自動フォローされない場合)、400エラー、500エラー — は false に設定されます。パースする前に必ず確認してください。

AbortController — タイムアウトとキャンセル

fetch() には組み込みのタイムアウトがありません。サーバーが転送途中で応答を停止した場合、 リクエストは無期限に待機し続けます。解決策は AbortController です:

js
async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeoutId  = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Usage
try {
  const data = await fetchWithTimeout('/api/products', {}, 5000);
} catch (err) {
  console.error(err.message); // "Request timed out after 5000ms"
}

AbortControllerは、ユーザーがページを移動したり、前の検索が完了する前に新しい検索を実行したりするときに、 実行中のリクエストをキャンセルするためにも役立ちます:

js
let searchController = null;

async function searchProducts(query) {
  // Cancel any previous search request
  if (searchController) {
    searchController.abort();
  }

  searchController = new AbortController();

  try {
    const response = await fetch(
      `/api/products/search?q=${encodeURIComponent(query)}`,
      { signal: searchController.signal }
    );
    return await response.json();
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
    return null; // request was cancelled — that's OK
  }
}

指数バックオフによるリトライ

ネットワークリクエストは一時的に失敗します — あるリトライでの503は次のリトライで成功することが多いです。 指数バックオフは標準的な戦略です: 過負荷のサーバーへの負荷を避けるために、リトライ間隔を徐々に長くしていきます:

js
async function fetchWithRetry(url, options = {}, retries = 3, baseDelayMs = 500) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Don't retry client errors (4xx) — they won't fix themselves
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error ${response.status} — not retrying`);
      }

      if (!response.ok) {
        throw new Error(`Server error ${response.status}`);
      }

      return await response.json();

    } catch (err) {
      const isLastAttempt = attempt === retries;

      if (isLastAttempt || err.message.includes('Client error')) {
        throw err;
      }

      // Exponential backoff with jitter: 500ms, 1000ms, 2000ms + random
      const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
      console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const products = await fetchWithRetry('/api/products', {}, 3, 500);
ジッターが重要な理由: 遅延に Math.random() * 100 を追加することで 「サンダリングハード」を防ぎます — サーバーの不具合の後、何千ものクライアントがまったく同じ瞬間にリトライする現象です。 小さなランダムオフセットで、大きな信頼性の向上が得られます。

インターセプターパターン — fetch()のラッピング

Axiosはインターセプターの概念を広めました: すべてのリクエストの前とすべてのレスポンスの後に実行されるフックです。 fetch() の薄いラッパーとして同じものを構築できます:

js
// api.js — your project's fetch wrapper
const API_BASE = 'https://api.example.com';

function getAuthToken() {
  return localStorage.getItem('authToken');
}

async function apiFetch(path, options = {}) {
  const url = `${API_BASE}${path}`;

  // Request interceptor — add auth header to every request
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers
  };

  const token = getAuthToken();
  if (token) headers['Authorization'] = `Bearer ${token}`;

  const response = await fetch(url, { ...options, headers });

  // Response interceptor — handle auth expiry globally
  if (response.status === 401) {
    logout(); // token expired — redirect to login
    throw new Error('Session expired');
  }

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    throw new Error(body.message ?? `API error ${response.status}`);
  }

  // Return null for 204 No Content
  if (response.status === 204) return null;

  return response.json();
}

// Usage — clean, no repeated boilerplate
const products = await apiFetch('/products');
const created  = await apiFetch('/products', {
  method: 'POST',
  body:   JSON.stringify({ name: 'New Product', price: 29.99 })
});

このパターンは認証、エラー処理、ベースURL設定を一元化します。 コードベース内のすべてのAPI呼び出しは、自動的に同じ動作を得られます。要件が変わっても — 新しいヘッダーの追加やJWTからセッションクッキーへの切り替えなど — 一箇所を更新するだけです。

異なるレスポンスタイプの処理

すべてのAPIがJSONを返すわけではありません。fetch() にはすべての一般的なレスポンスタイプに対応するメソッドがあります:

js
// JSON (most common)
const data = await response.json();

// Plain text (CSV, HTML, logs)
const csvText = await response.text();

// Binary data — download a file
const blob       = await response.blob();
const blobUrl    = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href     = blobUrl;
downloadLink.download = 'export.csv';
downloadLink.click();
URL.revokeObjectURL(blobUrl); // clean up

// ArrayBuffer — for WebAssembly or typed arrays
const buffer     = await response.arrayBuffer();
const byteArray  = new Uint8Array(buffer);

// Form data (multipart responses)
const formData = await response.formData();

CORS — 開発者が知っておくべきこと

CORS(Cross-Origin Resource Sharing)はサーバーではなくブラウザによって適用されます。 app.example.com のフロントエンドが api.example.com からfetchする場合、 ブラウザは Origin ヘッダーを送信し、権限のためにレスポンスを確認します。 クライアント側から制御できることは次のとおりです:

js
// credentials: 'include' — send cookies/session tokens cross-origin
// The server must also respond with Access-Control-Allow-Credentials: true
const response = await fetch('https://api.example.com/account', {
  credentials: 'include'
});

// credentials: 'same-origin' — default, only sends credentials to same origin
// credentials: 'omit' — never send credentials (useful for public CDN requests)

// mode: 'no-cors' — fire-and-forget for cross-origin requests
// You get a "opaque" response — no status, no body, no error
await fetch('https://analytics.example.com/event', {
  method: 'POST',
  mode:   'no-cors',
  body:   JSON.stringify({ event: 'page_view' })
});
// Use for analytics pings where you don't need the response

CORSエラーが発生している場合、修正はほぼ常にサーバー側にあります: サーバーが レスポンスヘッダーに Access-Control-Allow-Origin を追加する必要があります。 MDNのCORSガイド は設定方法についての最も詳細なリファレンスです。

便利なツール

fetchレスポンスをデバッグする際、JSON Formatter でAPIペイロードを読みやすくし、JSON Validatorで不正なレスポンスを検出できます。 クエリパラメータを安全にエンコードするには、JSON URL Encode が役立ちます。 MDN Using Fetchガイド は、すべてのfetchオプションとレスポンスメソッドの最良の参照先です。

まとめ

fetch() はJavaScriptでのHTTPリクエストの堅固な基盤ですが、 デフォルト設定では本番環境に必要なものがすべて欠けています。パースする前に必ず response.ok を確認してください。AbortController でタイムアウトを追加してください。 認証ヘッダー、エラーの正規化、401リダイレクトを一箇所で処理する薄いラッパー関数を構築してください。 一時的に失敗する可能性のあるリクエストには指数バックオフを使ったリトライロジックを追加してください。 この4つの習慣がデモコードと本番コードの違いです。