フォールトトレラント 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やネットワーク環境下でも、ユーザーに「壊れたアプリ」と思わせない、プロフェッショナルなフロントエンドが完成します。今日からあなたのプロジェクトのフェッチ層を見直し、より堅牢なデータ処理基盤を構築してください。

コメント