How do you make fetch retries not double-cache stale data?

Yo folks, I’m wiring up a tiny dev-tooling dashboard and I’m trying to make my data layer resilient without making tests flaky or the UI show old stuff after a retry.

const cache = new Map();

export async function getUser(id) {
  const key = `user:${id}`;
  if (cache.has(key)) return cache.get(key);

  const p = fetch(`/api/users/${id}`)
    .then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();
    })
    .catch(async err => {
      await new Promise(r => setTimeout(r, 200));
      return fetch(`/api/users/${id}`).then(r => r.json());
    });

  cache.set(key, p);
  return p;
}

If the first request fails and the retry succeeds, what’s a clean pattern to avoid caching a “poisoned” promise (or returning stale data) while still deduping in-flight requests?

Yoshiii :grinning_face_with_smiling_eyes:

Don’t cache the whole retry chain; cache only the current in-flight request and delete the entry on any failure so the next call starts clean.

If you also want to reuse good data, keep a separate value cache with a TTL and only write it after a successful r.ok JSON parse.

MechaPrime

Keep two caches: one for the in-flight promise (dedupe) and one for the stored value with a TTL.

Delete the in-flight entry on any reject, and only write the value cache after r.ok and a successful json() parse.

Sora

Also make sure the value cache key includes any request variant that affects the response (method, headers like auth/accept, query params), otherwise retries can “poison” a shared entry with the wrong payload.

Ellen

If your cache key is only the URL, a retry can stomp the entry with a totally different response.

Key it on method + query + Accept/Auth headers (and body hash for POST), and only write to cache after a verified 2xx JSON parse so a flaky attempt doesn’t poison it.

WaffleFries

Also add a request id or attempt number into your in-flight map so concurrent retries dedupe instead of racing, and use conditional requests (ETag/If-None-Match) so you never overwrite a fresh cache entry with an older payload.

BobaMilk

Late retry responses can arrive out of order, so guard the cache write with a monotonic fetchedAt and only commit if it’s newer than what’s already stored.

Hari