Handling Fetch Timeouts and Failed Requests in Server Components

Cap slow requests with AbortSignal.timeout and catch failures before they blank the page. Learn the timeout and error handling pattern.

7 min read

Handling fetch timeouts and failed requests in Next.js starts with one fact: a Server Component fetch can hang or reject, and neither should blank the page. Cap slow requests with a timeout and catch failures so the route still renders something useful. A timeout caps the worst case, and a catch or boundary turns the failure into UI instead of an exception.

App.tsxApp.tsx
// app/posts/page.tsx
type Post = { id: number; title: string };
 
export default async function PostsPage() {
  const res = await fetch("https://api.example.com/posts", { signal: AbortSignal.timeout(5000) });
  const posts: Post[] = await res.json();
  return <ul>{posts.map((post) => <li key={post.id}>{post.title}</li>)}</ul>;
}

AbortSignal.timeout aborts the request after five seconds. If the server does not respond in time, the fetch rejects with a timeout error instead of hanging forever.

One tradeoff comes with it: passing a signal opts the request out of Next.js request memoization, so two components fetching the same URL with a timeout each issue their own request.

Why requests fail

A fetch can fail for many reasons: a timeout, a network error, a DNS failure, or a non-success response. Timeouts and network errors reject, while a 404 or 500 resolves with a non-ok response that you must check yourself.

Checking res.ok

fetch only rejects on network and timeout errors. A response with status 500 resolves normally, so check res.ok and throw when the server reports an error. Without that check, a broken endpoint silently renders an empty list.

This is the failure mode readers hit most often, because the page looks fine in development against a healthy API and only breaks when the upstream service starts returning errors.

Catching expected failures

When a request can reasonably fail, wrap it in try/catch and return a fallback. This keeps the route responsive for the specific request that broke. A fallback can be as simple as a message or as rich as a cached partial list.

App.tsxApp.tsx
// app/posts/page.tsx
export default async function PostsPage() {
  try {
    const res = await fetch("https://api.example.com/posts", { signal: AbortSignal.timeout(5000) });
    if (!res.ok) throw new Error("Failed to load posts");
    const posts = await res.json();
    return <ul>{posts.map((post) => <li key={post.id}>{post.title}</li>)}</ul>;
  } catch (error) {
    console.error(error);
    return <p role="alert">Posts are unavailable right now.</p>;
  }
}

The page shows a message instead of crashing, and the role attribute tells assistive technology this is a status update rather than ordinary text. Logging in the catch keeps the failure visible in server logs instead of swallowing it silently.

When a non-ok response triggers the throw, the catch treats a server error the same as a timeout.

Catching unexpected errors with an error boundary

try/catch handles one known failure. An error boundary handles the rest, including errors thrown deep in the component tree. Add an error file to the segment and Next.js wraps the page automatically.

App.tsxApp.tsx
// app/posts/error.tsx
"use client";
 
type ErrorProps = { error: Error & { digest?: string }; retry: () => void };
 
export default function Error({ error, retry }: ErrorProps) {
  return (
    <div>
      <p>Something went wrong: {error.digest ?? "unknown error"}</p>
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

Error boundaries must be Client Components, which is why this file has the use client directive. The retry function, stable since Next.js 16.3, re-fetches and re-renders the segment, so a temporary failure clears without a full page reload.

Errors thrown on the server reach this component with a generic message and a digest that matches the server log entry, which is why the digest is what the UI shows. The older reset prop still exists, but it only clears the error state without re-fetching.

Choosing between catch and boundary

Use try and catch when one request has a known fallback, such as a list that can show an empty state. Use an error boundary when the failure should take down a whole section with a retry button, because boundaries also cover errors thrown in child components.

For the full error handling model, see error boundaries, and for structuring multiple requests see parallel vs sequential fetching.

Rune AI

Rune AI

Key Insights

  • AbortSignal.timeout caps how long a fetch can run.
  • try/catch handles failures you can recover from.
  • An error file catches unexpected errors per segment.
  • An error file is a Client Component with a retry prop.
  • A failed request should render a fallback, not a blank page.
RunePowered by Rune AI

Frequently Asked Questions

How do I time out a fetch?

Pass AbortSignal.timeout(milliseconds) as the signal option. The fetch rejects when the time runs out.

Should I catch every fetch error?

Catch failures you can recover from and return a fallback. Let an error boundary handle unexpected errors.

Can an error boundary be a Server Component?

No. Error boundaries must be Client Components, since a React error boundary needs client-side state and a retry handler.

Conclusion

Cap slow requests with AbortSignal.timeout and catch expected failures with try and catch. For anything unexpected, an error file gives the route a fallback and a retry action.