React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrerrorsretryerror-boundary

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 error value.
  • Error retry is enabled by default. SWR uses exponential backoff: retries at 1s, 2s, 4s, 8s, etc., capped by 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).
  • The onError(err, key, config) callback fires on every error, including retries.
  • Previous successful data is preserved in 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.

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 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.
  • Error retry is enabled by default with no max count. A permanently failing endpoint will retry indefinitely unless you set errorRetryCount.
  • onError fires 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

ApproachProsCons
SWR error returnDeclarative, per-componentMust handle in every component
Error BoundaryCatches render-time errorsDoes not catch async SWR errors natively
Global onError callbackCentralized error trackingCannot affect individual component rendering
Toast notificationsNon-blocking user feedbackUser 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 data and error in your component.
  • If both exist, render the data with a warning message.
  • Use isValidating to show a "Retrying..." indicator.