Frontend Labs
⋯

31. Mini project: a resilient fetch client (timeout, retry, cache, dedup)

class ResilientClient {
  #cache = new Map();
  #inFlight = new Map();   // dedupe: identical concurrent requests share one promise

  async get(url, { timeoutMs = 5000, retries = 2, cacheMs = 30000 } = {}) {
    const cached = this.#cache.get(url);
    if (cached && Date.now() - cached.time < cacheMs) return cached.data;

    if (this.#inFlight.has(url)) return this.#inFlight.get(url);   // join the existing request

    const promise = this.#fetchWithRetry(url, timeoutMs, retries)
      .then((data) => {
        this.#cache.set(url, { data, time: Date.now() });
        this.#inFlight.delete(url);
        return data;
      })
      .catch((err) => { this.#inFlight.delete(url); throw err; });

    this.#inFlight.set(url, promise);
    return promise;
  }

  async #fetchWithRetry(url, timeoutMs, retries) {
    for (let attempt = 0; attempt <= retries; attempt++) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeoutMs);
      try {
        const res = await fetch(url, { signal: controller.signal });
        clearTimeout(timer);
        if (!res.ok && res.status >= 500 && attempt < retries) {
          await new Promise(r => setTimeout(r, 2 ** attempt * 200));  // exponential backoff
          continue;
        }
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      } catch (err) {
        clearTimeout(timer);
        if (attempt === retries) throw err;
      }
    }
  }
}

This single class demonstrates request deduplication (multiple UI components asking for the same data at once share one network call), TTL caching, per-request timeouts via AbortController, and retry-with-backoff limited to retriable (5xx) failures — the exact pattern behind libraries like SWR and React Query, minus the React-specific rendering integration.


Generated from real-life-JS-guide.md · 8 Oct 2026 16:36