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/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/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/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
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.
Frequently Asked Questions
How do I time out a fetch?
Should I catch every fetch error?
Can an error boundary be a Server Component?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.