Error Handling
Recipe
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>;
}Working Example
"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>
);
}Deep Dive
How It Works
- SWR catches any error thrown or rejected promise from the fetcher and sets it as the
errorvalue. - Error retry is enabled by default. SWR uses exponential backoff: retries at 1s, 2s, 4s, 8s, etc., capped by
errorRetryInterval. shouldRetryOnErrorcan betrue,false, or a function(err) => booleanfor conditional retry logic.errorRetryCountlimits the total number of retry attempts (default: unlimited on slow connections).- The
onError(err, key, config)callback fires on every error, including retries. - Previous successful data is preserved in
dataeven when a revalidation fails. This meansdataanderrorcan both be defined simultaneously. onErrorRetrygives full control over retry behavior including timing and abort logic.
Variations
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>
);
}TypeScript Notes
- Pass the error type as the second generic:
useSWR<Data, Error>(key, fetcher). - Custom error classes give you type-safe access to extra fields.
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>
}Gotchas
- If your fetcher does not throw on non-2xx responses, SWR will never set
error. Always validateres.okin your fetcher. dataanderrorcan both be truthy at the same time. This happens when a revalidation fails but cached data exists. Do not assume they are mutually exclusive.- Error retry is enabled by default with no max count. A permanently failing endpoint will retry indefinitely unless you set
errorRetryCount. onErrorfires on every error event including retries, which can flood error reporting services. Debounce or deduplicate in your handler.- React Error Boundaries catch render errors, not async errors. SWR errors are async and will not be caught by Error Boundaries unless you re-throw during render.
Alternatives
| 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 |
FAQs
How does SWR's default error retry work?
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.
How do I prevent retries for specific HTTP status codes like 404?
const { data } = useSWR("/api/data", fetcher, {
shouldRetryOnError: (err) => err.status !== 404,
});Or use onErrorRetry for full control over retry logic per error type.
Gotcha: Can data and error both be defined at the same time?
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.
Why doesn't my React Error Boundary catch SWR errors?
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.
How do I create a custom error class for better error handling?
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.
How do I set up a global error handler for all SWR hooks?
<SWRConfig
value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
reportToSentry(error, { key });
}
},
}}
>
{children}
</SWRConfig>Gotcha: Why does onError fire repeatedly when retry is enabled?
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.
What happens if my fetcher does not throw on non-2xx responses?
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.
How do I type the error in useSWR with TypeScript?
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.
How can I show stale data with an error banner when revalidation fails?
- Check both
dataanderrorin your component. - If both exist, render the data with a warning message.
- Use
isValidatingto show a "Retrying..." indicator.