Error and loading states
What happens when things go wrong – and who catches it
error.tsx catches errors from an area, not-found.tsx handles missing addresses. Both are files, not configuration.
Without a safety net an error while fetching means a blank page. Next.js ships two files for that, which sit in the folder just like loading.tsx.
error.tsx
"use client"; // required – error.tsx is always a client component
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={reset}>Try again</button>
</div>
);
} The error.tsxCatches errors from the area below and shows something readable instead. Has to be a client component.→ file catches anything that goes wrong while rendering in its folder and below. The rest of the page – layout, header, navigation – stays put and usable.
app/blog/error.tsx catches errors from app/blog/page.tsx – but not from app/blog/layout.tsx. The level above is responsible for that. To protect a layout you need the error.tsx one step up.
Next.js replaces it with a generic one plus a digest identifier, so nothing internal leaks. Through that identifier you find the real error again in the server log.
not-found.tsx
import { notFound } from "next/navigation";
export default async function Page({ params }) {
const { slug } = await params;
const post = await loadPost(slug);
if (!post) notFound(); // stops here and shows not-found.tsx
return <article>{post.body}</article>;
} not-found.tsxShown when an address does not exist or your code calls `notFound()`.→ kicks in twice: when you call notFound(), and when an address does not exist at all. Both return a proper 404 status – important so search engines do not index the page.
const post = await loadPost(slug);
if (!post) throw new Error("Post not found"); const post = await loadPost(slug);
if (!post) notFound(); “Does not exist” is not a failure, it is a result. With throw you would get a 500 and the error page – the wrong signal for both users and search engines.
The levels at a glance
app/
error.tsx catches everything, including app/blog/layout.tsx
global-error.tsx last resort, replaces the root layout too
not-found.tsx 404 for the whole application
blog/
error.tsx catches errors from blog/page.tsx and below
loading.tsx placeholder while loading
page.tsx Because it replaces the root layout, it has to bring the HTML shell itself. It only kicks in when even the root layout fails – rare in practice, but the difference explains the unusual requirement.
Expected and unexpected errors
- +
notFound()for “does not exist” - +
Return values for validation errors – from a server action, say
- +
error.tsxfor the unexpected: database gone, third-party API silent
- −
throwfor everything you actually expect - −
Showing error details in the browser
- −
Skipping
error.tsxand hoping
▸ In more depth: what error.tsx does not catch optional
error.tsx catches:
✓ errors while rendering the page and below
✓ errors while fetching in those components
error.tsx does NOT catch:
✗ errors in its own layout → the level above handles it
✗ errors in event handlers → try/catch in the handler
✗ errors in server actions → return a value instead of throwing
✗ errors after rendering → in a setTimeout, for instance For server actions the return value is the better route: return { error: "…" } instead of throw. That way you can show the message in the form without replacing the whole view with an error page.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
Build the display Next.js shows when something fails. It shows a generic message and a button that rebuilds the area. (In Next.js the component would receive error and reset from outside; here you pass reset yourself so it is testable.)
- □The heading
Something went wrong.appears - □The technical error message is not shown
- □Clicking
Try againcallsreset
import { useState } from "react"; type Props = { error: Error & { digest?: string }; reset: () => void; }; // In Next.js: "use client" + the file is called error.tsx export function ErrorDisplay({ error, reset }: Props) { // TODO: generic message + a button calling reset return <div>???</div>; } export default function App() { const [attempts, setAttempts] = useState(0); const error = Object.assign(new Error("DB_CONN_REFUSED at 10.0.0.7"), { digest: "a1b2c3", }); return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <p data-testid="attempts">Attempts: {attempts}</p> <ErrorDisplay error={error} reset={() => setAttempts((a) => a + 1)} /> </div> ); }