【JS応用】フォールトトレラント JSON をフェッチする

フォールトトレラント JSON フェッチの設計思想

現代のフロントエンド開発において、外部APIとの通信は不可欠な要素です。しかし、ネットワークの不安定さ、予期せぬスキーマの変更、サーバー側の過負荷による不完全なレスポンスなど、フロントエンドエンジニアは常に「信頼できないデータ」と向き合わなければなりません。

「フォールトトレラント(耐障害性)」なJSONフェッチとは、単にリクエストを投げて成功・失敗を判定するだけではありません。データが壊れていたり、一部が欠損していたりしても、アプリケーションがクラッシュせず、可能な限りユーザー体験を損なわないように設計する手法を指します。本記事では、堅牢なデータフェッチ層を構築するための技術的アプローチを深掘りします。

なぜ標準のfetchだけでは不十分なのか

標準のfetch APIは、ネットワークエラーが発生しない限り、HTTPステータスコードが4xxや5xxであってもPromiseを解決してしまいます。また、JSONのパース処理(res.json())は、レスポンスボディが不正なJSON文字列であった場合に例外を投げ、アプリケーション全体の実行を停止させるリスクを孕んでいます。

プロフェッショナルな現場では、以下の3つのレイヤーで防御を行う必要があります。

1. 通信レイヤーの信頼性:タイムアウト、リトライ戦略、サーキットブレーカー。
2. パースレイヤーの安全性:例外をキャッチし、型安全な状態へ変換する。
3. ドメインレイヤーの適応性:バリデーションとデフォルト値によるデータの補完。

堅牢なフェッチ実装のアーキテクチャ

まず、通信の失敗を許容するラッパー関数を構築します。ここではリトライ処理とタイムアウトを組み込むことが重要です。


async function fetchWithRetry(url, options = {}, retries = 3, timeout = 5000) {
  for (let i = 0; i < retries; i++) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);

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

      if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
      
      const text = await response.text();
      try {
        return JSON.parse(text);
      } catch (e) {
        throw new Error("Invalid JSON format");
      }
    } catch (err) {
      clearTimeout(id);
      if (i === retries - 1) throw err;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // 指数バックオフ
    }
  }
}

Zodによるスキーマ駆動の型安全性

JSONをパースした後のデータが、期待したプロパティを持っているかを確認することはフォールトトレラントの要です。TypeScriptのインターフェースはコンパイル時にしか機能しません。実行時チェックを行うために、Zodなどのバリデーションライブラリを活用します。

Zodを使用することで、APIのレスポンスが予期せぬ構造であっても、例外を投げる代わりにデフォルト値を返したり、エラーをログに記録して「安全なフォールバック値」に置き換えることができます。


import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string().default('Anonymous'),
  email: z.string().email().optional(),
});

async function getSafeUser(url) {
  try {
    const rawData = await fetchWithRetry(url);
    // safeParseを使用することで、パースエラー時も例外を投げずに結果を取得
    const result = UserSchema.safeParse(rawData);
    
    if (!result.success) {
      console.error("Schema validation failed:", result.error);
      return { id: 0, name: 'Fallback User', email: '' }; // 安全な初期値
    }
    
    return result.data;
  } catch (err) {
    console.error("Network or parsing error:", err);
    return { id: 0, name: 'Offline User', email: '' };
  }
}

実務における高度な戦略:サーキットブレーカーとキャッシュ

大規模なアプリケーションでは、特定のAPIがダウンしている際に何度もリクエストを繰り返すと、クライアント側だけでなくサーバー側にも負荷をかけます。「サーキットブレーカー」パターンを実装し、一定回数のエラーが続いた場合は一時的にリクエストを遮断し、即座にキャッシュやデフォルト値を返す設計が望まれます。

また、SWRやTanStack Queryといったデータフェッチライブラリには、これらのフォールトトレラントな挙動がプリセットされています。実務では、これらをベースに独自の「エラーハンドリングポリシー」を注入するのが最も効率的です。

UIへの影響を最小限にする「グレースフル・デグラデーション」

データフェッチが失敗した際、画面全体をホワイトアウトさせるのは最悪のユーザー体験です。フォールトトレラントな設計において、UIコンポーネントは「データが欠損している状態」を許容するように構築されるべきです。

1. 部分的なエラー表示:リストの一部が読み込めない場合、その部分だけをエラー表示にし、他のコンテンツは表示し続ける。
2. ステイル・ホワイル・リバリデーション(SWR):最新データが取れない場合、ローカルストレージにある古いデータを一時的に表示する。
3. ユーザーへの通知:単にエラーを隠すのではなく、「現在オフラインのため一部の機能が制限されています」といった適切なフィードバックを行う。

実務アドバイス:エンジニアが意識すべき境界線

フォールトトレラントな実装を進める上で、最も重要なのは「どこまでを許容し、どこからをエラーとするか」の境界線です。

すべてをデフォルト値で埋めてしまうと、デバッグが極めて困難になります。以下の指針を推奨します。

– 開発環境では厳密なバリデーションエラーをコンソールに詳細出力する。
– 本番環境では、ユーザーの体験を阻害しない範囲で安全なフォールバック値を適用し、エラーログをSentryなどの監視ツールに送る。
– API側の変更に追従できるよう、スキーマ定義はバックエンドと共有可能なリポジトリ(またはOpenAPI定義からの自動生成)で管理する。

まとめ

フォールトトレラントなJSONフェッチは、単なるコードのテクニックではなく、アプリケーションのレジリエンス(回復力)を高めるための哲学です。

1. ネットワークエラーはリトライとタイムアウトで制御する。
2. JSONパースは例外をキャッチし、型安全なスキーマバリデーションを通す。
3. バリデーション失敗時はデフォルト値やフォールバックを用いてアプリを止めない。
4. UI側でデータ欠損を前提としたグレースフル・デグラデーションを実装する。

これらを組み合わせることで、どんなに不安定なAPIやネットワーク環境下でも、ユーザーに「壊れたアプリ」と思わせない、プロフェッショナルなフロントエンドが完成します。今日からあなたのプロジェクトのフェッチ層を見直し、より堅牢なデータ処理基盤を構築してください。

コメント

タイトルとURLをコピーしました