Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 11 min read

Error and loading states

What happens when things go wrong – and who catches it

In one sentence

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

app/blog/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>
  );
}
reset() rebuilds the failed area – without reloading the whole page.

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.

Pitfall It does not catch its own layout

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.

Careful In production you do not see the message

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

app/blog/[slug]/page.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.

A missing record
looks like a failure
const post = await loadPost(slug);
if (!post) throw new Error("Post not found");
is a 404
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

levels.txt
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
Good to know global-error.tsx needs its own html and body

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

Reach for it when …
  • +

    notFound() for “does not exist”

  • +

    Return values for validation errors – from a server action, say

  • +

    error.tsx for the unexpected: database gone, third-party API silent

Skip it when …
  • throw for everything you actually expect

  • Showing error details in the browser

  • Skipping error.tsx and hoping

In more depth: what error.tsx does not catch optional
limits.txt
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

An error display with retry

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 again calls reset
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>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →