What a retry may undo
A GET retries safely. A POST retries a side effect: two orders, two emails, two everything. Idempotency is the property that makes retry boring, and it is designed server-side before the client's backoff loop is written.
The one-retry rule
The harness fetches hundreds of pages through a keep-alive pool; the server closing an idle socket arrives as a socket error, not a status. One retry on a fresh connection recovers exactly that population — and a second failure is real, so it throws.
The discipline is in the 'only': retry transport errors, never 4xx. A 404 on the second attempt is the same 404, and retrying it doubles the embarrassment.
const fetchRetry = async (url, init, attempt = 1) => {
try { return await fetch(url, init) }
catch (err) { // transport only — a 404 never lands here
if (attempt < 2) return fetchRetry(url, init, attempt + 1)
throw err
}
}