Skip to content

Advanced Fetcher with Retry and Timeout

A production-ready HTTP fetcher with advanced error handling, automatic retries, configurable timeouts, and abort functionality. Perfect for real-world applications where network reliability matters.

This advanced fetcher demonstrates sophisticated state machine patterns for HTTP requests:

  • Configurable retry logic - Automatic retries with exponential backoff for network errors, timeouts, and failures
  • Request timeout management - Configurable timeout with automatic cleanup
  • Abort capability - Cancel in-flight requests with proper cleanup
  • Network error detection - Distinguishes between network errors, timeouts, and other failures
  • Detailed state tracking - Comprehensive state management for the entire fetch lifecycle
  • Live configuration - Change settings and see the machine recreate with new options

The fetcher uses effects and guards to implement complex retry logic while maintaining clean state transitions.

import {
createMachine,
defineStates,
delay,
effect,
enter,
guard,
whenEventType,
whenState,
assignEventApi,
setup,
} from "matchina";
import { autotransition } from "../lib/autotransition";
type Options = RequestInit & {
timeout?: number;
maxTries?: number;
autoretry?: boolean;
getData?: (response: Response) => any;
};
export function createFetcher(
defaultUrl: string,
defaultOptions: Options = {}
) {
const states = defineStates({
Idle: undefined,
Fetching: (url = defaultUrl, options = defaultOptions) => ({
url,
options,
abort: new AbortController(),
}),
ProcessingResponse: (response: Response) => response,
Resolved: (data: any) => data,
Error: (error: Error) => error,
NetworkError: (error: Error) => error,
Aborted: undefined,
TimedOut: undefined,
Refetching: undefined,
});
const canRefetch = { refetch: "Refetching" } as const;
const machine = createMachine(
states,
{
Idle: {
fetch: "Fetching",
},
Fetching: {
fetched: "ProcessingResponse",
reject: "Error",
abort: "Aborted",
timeout: "TimedOut",
networkError: "NetworkError",
},
ProcessingResponse: {
...canRefetch,
resolve: "Resolved",
error: "Error",
},
Resolved: canRefetch,
Error: canRefetch,
NetworkError: canRefetch,
Aborted: canRefetch,
TimedOut: canRefetch,
Refetching: { "": "Fetching" },
},
"Idle"
);
const fetcher = Object.assign(assignEventApi(machine), {
fetch: (url?: string, options?: RequestInit) => {
fetcher.fetch(url, options);
return fetcher.promise!;
},
promise: undefined as undefined | Promise<Response>,
done: undefined as undefined | Promise<void>,
tries: 0,
});
const maxTries = defaultOptions.maxTries ?? Number.POSITIVE_INFINITY;
const runFetch = async (url: string, options: RequestInit) => {
fetcher.tries = fetcher.tries + 1;
const promise = (fetcher.promise = fetch(url, options));
try {
const res = await promise;
if (fetcher.promise === promise) {
fetcher.fetched(res.clone());
}
} catch (error) {
const { name } = error as Error;
if (name === "AbortError") {
fetcher.tries--;
} else if (name === "TypeError") {
fetcher.networkError(error as Error);
} else {
fetcher.reject(error as Error);
}
} finally {
delete fetcher.promise;
}
};
const resolveResponseData = (response: Response) => response.json();
setup(fetcher)(
enter(
whenState("Fetching", (ev) => {
(ev as any).promise = runFetch(ev.to.data.url, {
...ev.to.data.options,
signal: ev.to.data.abort.signal,
});
const { timeout } = ev.to.as("Fetching").data.options;
if (timeout) {
const timer = setTimeout(fetcher.timeout, timeout);
return () => clearTimeout(timer);
}
})
),
effect(
whenState("ProcessingResponse", (ev) => {
fetcher.tries = 0;
delay(1000).then(() => {
resolveResponseData(ev.to.data).then(fetcher.resolve);
});
})
),
autotransition(),
effect(
whenEventType("abort", (ev) => {
ev.from.data.abort.abort();
})
),
guard((ev) => (ev.type === "refetch" ? fetcher.tries < maxTries : true))
);
if (defaultOptions.autoretry) {
const autoRetryStates = ["NetworkError", "TimedOut", "Error"] as const;
setup(fetcher)(
...autoRetryStates.map((stateName) =>
effect(
whenState(stateName, () => {
if (fetcher.tries < maxTries) {
const backoff = 1000 * fetcher.tries;
const timer = setTimeout(() => {
fetcher.refetch();
}, backoff);
return () => {
clearTimeout(timer);
};
}
})
)
)
);
}
return fetcher;
}
// Removed unused type
export type FetcherMachine = ReturnType<typeof createFetcher>;

The advanced fetcher extends the basic fetch pattern with sophisticated error handling:

  1. Idle - Ready to make a request with configurable options
  2. Fetching - Request in flight with timeout and abort capability
  3. ProcessingResponse - Parsing response data with error handling
  4. Resolved - Request succeeded, displays success state
  5. Error/NetworkError/TimedOut - Various failure states with retry capability
  6. Refetching - Automatic retry with exponential backoff
  7. Aborted - User-cancelled request

The configuration form recreates the machine when options change, demonstrating how to handle dynamic machine configuration in React applications.

Now that you’ve seen an advanced fetcher, you might want to explore: