Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
SWR catches errors thrown by your fetcher and exposes them via the error return value. Configure automatic retries, error callbacks, and integrate with React Error Boundaries for robust error handling.
"use client";
import useSWR from "swr";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error("An error occurred while fetching data.");
(error as any).info = await res.json();
(error as any).status = res.status;
throw error;
}
return res.json();
};
function UserProfile({ id }: { id: string }) {
const { data, error } = useSWR(`/api/users/${id}`, fetcher, {
onError: (err) => console.error("SWR error:", err),
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 5000,
});
if (error) return <div>Error: {error.message}</div>;
return <div>{data?.name}</div>;
}"use client";
import useSWR from "swr";
import { Component, ReactNode } from "react";
// Custom error class with extra context
class ApiError extends Error {
status: number;
info: Record<string, unknown>;
constructor(message: string, status: number, info: Record<string, unknown>) {
super(message);
this.status = status;
this.info = info;
}
}
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const info = await res.json().catch(() => ({}));
throw new ApiError(
`API error: ${res.statusText}`,
res.status,
info
);
}
return res.json();
};
// Error Boundary component
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
function OrderDetails({ orderId }: { orderId: string }) {
const { data, error, isLoading } = useSWR(`/api/orders/${orderId}`, fetcher, {
shouldRetryOnError: (err: ApiError) => err.status !== 404,
errorRetryCount: 3,
errorRetryInterval: 2000,
onError: (err: ApiError) => {
if (err.status === 401) {
window.location.href = "/login";
}
},
});
if (isLoading) return <div>Loading order...</div>;
if (error) {
if (error instanceof ApiError && error.status === 404) {
return <div>Order not found</div>;
}
return <div>Something went wrong: {error.message}</div>;
}
return (
<div>
<h2>Order #{data.id}</h2>
<p>Status: {data.status}</p>
<p>Total: ${data.total}</p>
</div>
);
}
export default function OrderPage({ orderId }: { orderId: string }) {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<OrderDetails orderId={orderId} />
</ErrorBoundary>
);
}error value.errorRetryInterval.shouldRetryOnError can be true, false, or a function (err) => boolean for conditional retry logic.errorRetryCount limits the total number of retry attempts (default: unlimited on slow connections).onError(err, key, config) callback fires on every error, including retries.data even when a revalidation fails. This means data and error can both be defined simultaneously.onErrorRetry gives full control over retry behavior including timing and abort logic.Custom retry with backoff:
const { data } = useSWR("/api/data", fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Never retry on 404
if (error.status === 404) return;
// Stop after 5 retries
if (retryCount >= 5) return;
// Exponential backoff
setTimeout(() => revalidate({ retryCount }), Math.min(1000 * 2 ** retryCount, 30000));
},
});Global error handler:
<SWRConfig
value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
reportToSentry(error, { key });
}
},
}}
>
{children}
</SWRConfig>Error state with stale data:
function Dashboard() {
const { data, error, isValidating } = useSWR("/api/stats", fetcher);
return (
<div>
{error && (
<div className="bg-yellow-100 p-2">
Failed to refresh. Showing last known data.
{isValidating && " Retrying..."}
</div>
)}
{data && <StatsDisplay stats={data} />}
</div>
);
}useSWR<Data, Error>(key, fetcher).const { data, error } = useSWR<User, ApiError>("/api/me", fetcher);
if (error) {
// error is typed as ApiError
console.log(error.status); // number
console.log(error.info); // Record<string, unknown>
}error. Always validate res.ok in your fetcher.data and error can both be truthy at the same time. This happens when a revalidation fails but cached data exists. Do not assume they are mutually exclusive.errorRetryCount.onError fires on every error event including retries, which can flood error reporting services. Debounce or deduplicate in your handler.| Approach | Pros | Cons |
|---|---|---|
| SWR error return | Declarative, per-component | Must handle in every component |
| Error Boundary | Catches render-time errors | Does not catch async SWR errors natively |
| Global onError callback | Centralized error tracking | Cannot affect individual component rendering |
| Toast notifications | Non-blocking user feedback | User might miss the notification |
SWR retries with exponential backoff: 1s, 2s, 4s, 8s, etc., capped by errorRetryInterval. Retry is enabled by default with no max count unless you set errorRetryCount.
const { data } = useSWR("/api/data", fetcher, {
shouldRetryOnError: (err) => err.status !== 404,
});Or use onErrorRetry for full control over retry logic per error type.
Yes. When a revalidation fails but cached data exists, both data and error are truthy. Do not assume they are mutually exclusive. Show stale data with an error banner for the best user experience.
Error Boundaries catch errors during rendering, not async errors. SWR errors are asynchronous and will not propagate to Error Boundaries unless you re-throw during render or use suspense: true mode.
class ApiError extends Error {
status: number;
info: Record<string, unknown>;
constructor(message: string, status: number, info: Record<string, unknown>) {
super(message);
this.status = status;
this.info = info;
}
}Throw it in your fetcher when res.ok is false.
<SWRConfig
value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
reportToSentry(error, { key });
}
},
}}
>
{children}
</SWRConfig>onError fires on every error event, including each retry attempt. This can flood error reporting services. Debounce or deduplicate error reports in your handler, or limit retries with errorRetryCount.
SWR will never set error. The response body is treated as successful data. Always check res.ok in your fetcher and throw an error for non-2xx status codes.
Pass the error type as the second generic:
const { data, error } = useSWR<User, ApiError>("/api/me", fetcher);
if (error) {
console.log(error.status); // typed as number
}If you omit the error generic, it defaults to any.
data and error in your component.isValidating to show a "Retrying..." indicator.Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥