javascript 166 lines · 4 tabs

Retry Failed React Mutations with Exponential Backoff and a Status Component

Shared by codesnips Sep 2026
4 tabs
const DEFAULTS = {
  baseDelay: 300,
  maxDelay: 8000,
  factor: 2,
  jitter: 0.5,
};

export function computeDelay(attempt, opts = {}) {
  const { baseDelay, maxDelay, factor, jitter } = { ...DEFAULTS, ...opts };
  const raw = baseDelay * Math.pow(factor, attempt);
  const capped = Math.min(raw, maxDelay);
  const rand = 1 - jitter + Math.random() * jitter * 2;
  return Math.round(capped * rand);
}

export function isRetryable(error) {
  if (error && error.name === 'AbortError') return false;
  const status = error && error.status;
  if (status == null) return true; // network / fetch failure
  if (status === 429) return true;
  return status >= 500 && status < 600;
}

export function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    if (signal) {
      signal.addEventListener('abort', () => {
        clearTimeout(id);
        reject(new DOMException('Aborted', 'AbortError'));
      }, { once: true });
    }
  });
}
4 files · javascript Explain with highlit

This snippet shows how to make write requests (POST/PUT/DELETE) resilient in a React app by wrapping fetch in a custom useMutation hook that retries transient failures with exponential backoff, and pairing it with a small MutationStatus component that surfaces retry progress to the user.

The core idea is that network calls fail for reasons that are often temporary: a flaky connection, a 503 during a deploy, or a rate limit. Retrying immediately usually just hammers an already-struggling server. Exponential backoff spaces retries out — roughly base * 2^attempt — so each successive wait doubles, and adding jitter avoids the thundering-herd problem where many clients retry in lockstep.

In backoff.js, computeDelay calculates the wait for a given attempt, capping it at maxDelay and mixing in random jitter. isRetryable decides whether an error is worth retrying at all: network errors and 5x/429 responses are retried, while 4xx client errors (a bad payload, a 403) are not, since replaying them changes nothing. This distinction matters — blindly retrying everything wastes time and can mask real bugs.

In useMutation.js, the hook exposes a mutate function plus reactive status, error, and attempt state. The retry loop lives in run, which awaits computeDelay between tries and bails early on non-retryable errors or when maxAttempts is exhausted. It also wires an AbortController so an in-flight request can be cancelled, and a mountedRef guard prevents state updates after the component unmounts — a common source of React warnings. The reducer-driven state keeps status transitions explicit: idle, loading, retrying, success, error.

In MutationStatus.jsx, the presentational component reads that state to show a spinner, a retry counter like Retrying (2/4)…, or a failure message with a manual retry button. Keeping presentation separate from the retry mechanics means the same hook can drive very different UIs.

A typical use is a save-profile form or a checkout submit where losing the write is worse than waiting a moment. The trade-off is latency on the unhappy path and the need to keep mutations idempotent, since a retried request may actually have succeeded server-side before the response was lost.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    url: String,
    delay: { type: Number, default: 800 },

Stimulus: autosave draft with Turbo-friendly requests

rails stimulus hotwire
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Retry Failed React Mutations with Exponential Backoff and a Status Component — share card
Link copied