Design wechseln

Next.js 404- und 500-Seiten anpassen: Vom technischen Setup bis zum Design

Easton editorial illustration: monorepo project desk

Freitagnachmittag, 15 Uhr – der Product Manager postet plötzlich einen Screenshot in den Chat: „Ist das unsere Website? Sieht ja schrecklich aus!“

Ich klicke drauf – weißer Hintergrund, schwarzer Text, das nackte „404 This page could not be found“. Peinlich.

Erst als die Zahlen kamen, wurde es ernst: 40 % der Nutzer schlossen den Tab, nachdem sie die Standard-404-Seite gesehen hatten.

Bei Next.js-Projekten konzentrieren wir uns meist auf die „normalen“ Seiten: die Startseite soll schön sein, Listen flüssig, Detailseiten perfekt. Fehlerseiten? Wer kümmert sich schon darum – die sieht man selten.

Diese Zahl hat mich wachgerüttelt: Fehlerseiten sind kein optionales Extra, sondern Ihre letzte Chance, Nutzer zu halten. Stellen Sie sich vor, jemand klickt auf einen toten Link, will eigentlich noch auf Ihrer Seite stöbern – und sieht dann eine langweilige weiße Seite mit dem kalten Hinweis „Seite nicht gefunden“. Keine Navigation, kein Suchfeld, kein Hinweis. Was denkt der Nutzer? „Ist diese Website überhaupt seriös?“

Zum Glück bietet der Next.js App Router ein vollständiges Fehlerbehandlungssystem: not-found.tsx für 404, error.tsx für Laufzeitfehler, global-error.tsx als Fallback für die gesamte App. Klingt einfach? Die Fallstricke sind zahlreich.

Beim ersten Setup lieferte der HTTP-Status hartnäckig 200 statt 404 – Google indexierte meine 404-Seiten gar nicht mehr. Ein anderes Mal wollten die Styles in global-error.tsx einfach nicht greifen; erst nach stundenlangem Dokumentationslesen wurde klar: CSS-Modul-Imports werden dort nicht unterstützt.

In diesem Artikel führe ich Sie Schritt für Schritt durch die Next.js-Fehlerseiten – von der Grundnutzung von not-found.tsx über Error Boundaries in error.tsx bis hin zum Design einer 404-Seite, die Nutzer wirklich hält. Der Code ist vollständig, die Fallen habe ich selbst ausprobiert – Sie können direkt loslegen.

Next.js-Fehlerbehandlung im Überblick

Als ich den App Router das erste Mal nutzte, war mir lange unklar, was die drei Dateien eigentlich unterscheidet. not-found.tsx, error.tsx, global-error.tsx – die Namen klingen ähnlich, die Aufgaben sind völlig verschieden.

Die drei Fehlerdateien im Überblick

Kurz gesagt:

  • not-found.tsx – speziell für 404, wenn eine Seite nicht existiert
  • error.tsx – für Laufzeitfehler, z. B. fehlgeschlagene Datenladung oder Codefehler
  • global-error.tsx – letztes Sicherheitsnetz, wenn sogar das Root-Layout ausfällt

Warum drei Dateien? Reicht nicht eine error.tsx?

Next.js behandelt Fehler hierarchisch – wie Matroschka-Puppen. error.tsx fängt nur Fehler auf derselben Ebene und in Unterrouten ab, nicht Fehler im eigenen layout.tsx. Was, wenn das Root-Layout selbst scheitert? Dafür brauchen Sie global-error.tsx.

not-found.tsx ist etwas Besonderes – es hat sogar höhere Priorität als error.tsx. Wenn Sie aktiv notFound() aufrufen, überspringt Next.js error.tsx und rendert direkt not-found.tsx.

Die Dateiposition ist entscheidend

Alle drei Dateien können auf verschiedenen Routing-Ebenen liegen – die Position bestimmt den Gültigkeitsbereich.

Fehlerdateien auf Root-Ebene (im app/-Verzeichnis):

app/
├── layout.tsx
├── not-found.tsx        ← Globale 404-Seite
├── error.tsx            ← Globales Error Handling
├── global-error.tsx     ← Fallback für Root-Layout
└── page.tsx

Fehlerdateien auf Route-Ebene (unter einer bestimmten Route):

app/
├── blog/
│   ├── [slug]/
│   │   ├── page.tsx
│   │   ├── not-found.tsx    ← Blog-spezifische 404
│   │   └── error.tsx         ← Blog-spezifische Fehlerseite

Ruft ein Nutzer /blog/nicht-existierender-artikel auf, zeigt Next.js zuerst app/blog/[slug]/not-found.tsx – nicht die globale app/not-found.tsx. So können Sie pro Modul unterschiedliche Fehlerseiten gestalten.

notFound()-Funktion: 404 programmatisch auslösen

Eine not-found.tsx-Datei allein reicht nicht – Sie müssen wissen, wann sie ausgelöst wird.

Typisches Szenario: Daten anhand einer ID laden, aber die Daten existieren nicht.

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  if (!res.ok) return null
  return res.json()
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()  // not-found.tsx auslösen
  }

  return <article>{post.title}</article>
}

Achtung, ein häufiger Fehler: notFound() muss aufgerufen werden, bevor irgendein JSX zurückgegeben wird. Wenn Sie bereits Teilinhalte zurückgeben, hat das Streaming begonnen – der HTTP-Status bleibt dann bei 200 statt 404.

Genau das ist mir passiert:

// Falsches Beispiel
export default async function Page({ params }) {
  const data = await fetchData(params.id)

  return (
    <div>
      {!data ? notFound() : <Content data={data} />}  // JSX läuft schon!
    </div>
  )
}

Die 404-Seite erschien zwar, aber der HTTP-Status war 200 – Suchmaschinen indexierten das als normale Seite. SEO kaputt.

Richtige Variante:

export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound()  // Erst prüfen, dann aufrufen
  }

  return <Content data={data} />  // JSX nur bei vorhandenen Daten
}

Erst Daten validieren, bei Fehler sofort notFound(), dann erst JSX zurückgeben. So ist der Status korrekt 404.

not-found.tsx: Benutzerdefinierte 404-Seite in der Praxis

Theorie genug – jetzt wird gebaut. Wir starten mit einer Basisversion und erweitern sie Schritt für Schritt.

Basisversion: Erst mal funktionsfähig

Die einfachste not-found.tsx sieht so aus:

// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center">
        <h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
        <p className="text-xl text-gray-600 mb-8">
          Entschuldigung, die angeforderte Seite existiert nicht
        </p>
        <Link
          href="/"
          className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          Zur Startseite
        </Link>
      </div>
    </div>
  )
}

Speichern, einen nicht existierenden Pfad aufrufen – z. B. http://localhost:3000/nicht-existierende-seite – und schon sieht es besser aus als das Standard-Weiß-Schwarz.

Aber noch zu simpel: Ein einziger „Zur Startseite“-Button reicht nicht, wenn der Nutzer etwas Bestimmtes sucht.

Erweiterte Version: Mehr Auswege für Nutzer

Eine gute 404-Seite bietet mehrere Wege weiter:

  1. Suchfeld – Nutzer finden selbst, was sie suchen
  2. Beliebte Links – Hinweis auf populäre Inhalte
  3. Markenelemente – Logo, Markenfarben, konsistentes Erscheinungsbild

Der vollständige Code:

// app/not-found.tsx
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

export default function NotFound() {
  const router = useRouter()
  const [searchQuery, setSearchQuery] = useState('')

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    if (searchQuery.trim()) {
      router.push(`/search?q=${encodeURIComponent(searchQuery)}`)
    }
  }

  const popularLinks = [
    { href: '/blog', label: 'Tech-Blog' },
    { href: '/projects', label: 'Projekte' },
    { href: '/about', label: 'Über uns' },
  ]

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="max-w-2xl w-full px-6 py-12 text-center">
        {/* Große 404 */}
        <h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 mb-4">
          404
        </h1>

        {/* Freundliche Fehlermeldung */}
        <p className="text-2xl font-medium text-gray-800 mb-2">
          Hoppla, diese Seite ist verschwunden
        </p>
        <p className="text-gray-600 mb-8">
          Der Link ist vielleicht abgelaufen oder die Seite wurde verschoben.<br/>
          Keine Sorge – probieren Sie eine der folgenden Optionen:
        </p>

        {/* Suchfeld */}
        <form onSubmit={handleSearch} className="mb-8">
          <div className="flex gap-2 max-w-md mx-auto">
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder="Suchen Sie nach Inhalten..."
              className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
            <button
              type="submit"
              className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
            >
              Suchen
            </button>
          </div>
        </form>

        {/* Beliebte Links */}
        <div className="mb-8">
          <p className="text-sm text-gray-600 mb-4">Oder besuchen Sie diese beliebten Seiten:</p>
          <div className="flex flex-wrap justify-center gap-3">
            {popularLinks.map((link) => (
              <Link
                key={link.href}
                href={link.href}
                className="px-5 py-2 bg-white text-gray-700 rounded-lg border border-gray-200 hover:border-blue-500 hover:text-blue-600 transition-colors"
              >
                {link.label}
              </Link>
            ))}
          </div>
        </div>

        {/* Zur Startseite */}
        <Link
          href="/"
          className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
        >
          <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
          </svg>
          Zur Startseite
        </Link>
      </div>
    </div>
  )
}

Am Dateianfang steht 'use client'. Warum? Suchfeld braucht useState und useRouter – das sind Client-Funktionen, die Komponente muss als Client Component deklariert werden.

Diese Version ist deutlich besser. Nutzer können:

  • direkt suchen
  • beliebte Links anklicken
  • im Notfall zur Startseite zurück

Die Absprungrate sinkt spürbar.

Fortgeschritten: 404-Fehler tracken

Wenn Sie wissen wollen, welche nicht existierenden Seiten Nutzer aufrufen (vielleicht fehlen Inhalte, die Sie anlegen sollten), fügen Sie Tracking hinzu:

'use client'

import { useEffect } from 'react'
import { usePathname } from 'next/navigation'

export default function NotFound() {
  const pathname = usePathname()

  useEffect(() => {
    // An Ihr Analytics-Tool senden
    if (typeof window !== 'undefined') {
      // Google Analytics Beispiel
      window.gtag?.('event', 'page_not_found', {
        page_path: pathname,
      })

      // Oder an Ihren eigenen Server
      fetch('/api/analytics/404', {
        method: 'POST',
        body: JSON.stringify({ path: pathname }),
      }).catch(() => {}) // Fehler ignorieren – UX nicht beeinträchtigen
    }
  }, [pathname])

  return (
    // ... Ihre 404-UI
  )
}

Nach einiger Zeit sehen Sie vielleicht:

  • Viele Nutzer suchen eine gelöschte alte Seite → 301-Weiterleitung einrichten
  • Ein URL-Tippfehler tritt sehr häufig auf → automatische Korrektur
  • Bestimmte Inhalte werden oft gesucht → fehlende Inhalte ergänzen

error.tsx und global-error.tsx: 500-Fehler behandeln

not-found.tsx deckt nur „Seite nicht gefunden“ ab. Was ist mit Codefehlern, ausgefallenen APIs oder Datenbankproblemen? Dafür kommt error.tsx ins Spiel.

Grundlagen von error.tsx

error.tsx muss eine Client Component sein – die erste Zeile ist 'use client'.

Warum zwingend Client? React Error Boundaries laufen nur im Browser, Server-seitig geht das nicht.

// app/error.tsx
'use client'

import Link from 'next/link'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full px-6 py-8 bg-white rounded-lg shadow-lg">
        <div className="text-center">
          <div className="text-6xl mb-4">⚠️</div>
          <h2 className="text-2xl font-bold text-gray-900 mb-2">Etwas ist schiefgelaufen!</h2>
          <p className="text-gray-600 mb-6">
            Beim Laden der Seite ist ein Fehler aufgetreten
          </p>

          <button
            onClick={() => reset()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
          >
            Erneut versuchen
          </button>

          <Link
            href="/"
            className="block mt-4 text-sm text-gray-500 hover:text-gray-700"
          >
            Zur Startseite
          </Link>
        </div>
      </div>
    </div>
  )
}

Zwei wichtige Parameter:

  • error – das erfasste Fehlerobjekt mit message und digest (Fehler-Hash)
  • reset – Funktion, die das Route-Segment neu rendert und einen Wiederherstellungsversuch startet

Ein Klick auf „Erneut versuchen“ führt reset() aus. Bei vorübergehenden Netzwerkfehlern kann das ausreichen.

Fehlerinformationen in der Produktion

Sicherheitsaspekt: In der Entwicklung zeigt error.message den vollständigen Fehler – z. B. „Database connection failed: invalid credentials“.

In der Produktion darf das nicht passieren – sensible Daten könnten durchsickern.

Next.js maskiert automatisch. Das error-Objekt enthält nur noch:

  • message – allgemeine Fehlermeldung ohne Details
  • digest – Fehler-Hash zum Abgleich mit Server-Logs

Die echten Details landen in den Server-Logs. Mit digest finden Sie sie:

'use client'

export default function Error({ error }: { error: Error & { digest?: string } }) {
  return (
    <div>
      <h2>Etwas ist schiefgelaufen</h2>
      <p>{error.message}</p>
      {error.digest && (
        <p className="text-xs text-gray-400 mt-4">
          Fehler-ID: {error.digest}
        </p>
      )}
    </div>
  )
}

Der Nutzer sieht „Fehler-ID: abc123“, schickt Ihnen einen Screenshot – und Sie finden den vollständigen Stack in den Logs.

Fehler an Monitoring-Dienste senden

Online-Fehler sollten nicht auf Nutzer-Feedback warten. Besser: automatisch an Sentry, Datadog oder ähnliche Dienste melden.

'use client'

import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Fehler an Sentry senden
    Sentry.captureException(error)
  }, [error])

  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h2>Etwas ist schiefgelaufen!</h2>
        <button onClick={() => reset()}>Erneut versuchen</button>
      </div>
    </div>
  )
}

useEffect feuert einmal bei Fehler und sendet die vollständigen Infos an Sentry. Im Dashboard sehen Sie:

  • Stack Trace
  • Browser-Informationen
  • Route zum Fehlerzeitpunkt
  • Zeitstempel

Probleme erkennen Sie in Minuten – nicht erst bei Beschwerden.

global-error.tsx: Das letzte Sicherheitsnetz

error.tsx ist mächtig, hat aber eine Lücke: Fehler im eigenen layout.tsx fängt es nicht ab.

Dafür gibt es global-error.tsx – es umschließt die gesamte App und fängt sogar Root-Layout-Fehler ab.

// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div style={{ padding: '50px', textAlign: 'center' }}>
          <h2>Die Website ist auf einen schweren Fehler gestoßen</h2>
          <p>Wir arbeiten an der Behebung – bitte versuchen Sie es später erneut</p>
          <button onClick={() => reset()}>Erneut versuchen</button>
        </div>
      </body>
    </html>
  )
}

Drei wichtige Punkte:

  1. <html> und <body> sind Pflicht
    Wenn das Root-Layout ausfällt, ersetzt global-error.tsx es vollständig. Sie müssen die HTML-Struktur selbst bereitstellen.

  2. Keine CSS-Module oder globalen Styles importieren
    Next.js ignoriert CSS-Imports in global-error.tsx. Nur Inline-Styles oder <style>-Tags.

  3. Selten ausgelöst
    Root-Layouts sind meist simpel – echte Auslöser sind selten. global-error.tsx ist eher eine Versicherung.

Trotzdem empfehle ich, sie anzulegen. Im Ernstfall ist das besser als ein weißer Bildschirm.

Vollständiges global-error.tsx-Beispiel

Mit etwas Styling sieht es weniger roh aus:

// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <style>{`
          * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
          }
          body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          }
          .container {
            text-align: center;
            color: white;
            padding: 2rem;
          }
          h2 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
          }
          p {
            font-size: 1.2rem;
            margin-bottom: 2rem;
            opacity: 0.9;
          }
          button {
            padding: 12px 32px;
            font-size: 1rem;
            background: white;
            color: #667eea;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: 600;
          }
          button:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
          }
        `}</style>

        <div className="container">
          <h2>😵 Schwerer Systemfehler</h2>
          <p>Es tut uns leid – die Website ist auf ein unerwartetes Problem gestoßen<br/>Unser Team wurde benachrichtigt und arbeitet an der Behebung</p>
          <button onClick={() => reset()}>Neu laden</button>
          <p style={{ fontSize: '0.875rem', marginTop: '2rem', opacity: 0.7 }}>
            Fehler-ID: {error.digest || 'unknown'}
          </p>
        </div>
      </body>
    </html>
  )
}

Kein Tailwind, keine CSS-Datei – Styles nur im <style>-Tag. Etwas altmodisch, aber funktional.

Design-Best-Practices: Nutzer auf Fehlerseiten halten

Code fertig – aber noch nicht abschließen. Technische Umsetzung ist Schritt eins; über Bleiben oder Gehen entscheidet das Design.

Ich habe 404-Seiten von Spotify, Figma und Mailchimp analysiert – gemeinsame Muster:

Pflichtelemente: Auswege bieten

Eine solide Fehlerseite braucht mindestens:

1. Klare, aber nicht einschüchternde Fehlermeldung

❌ Nicht so:

Error 404: The requested resource could not be located on the server.

Wer versteht das? Nutzer denken: „Ist die Website kaputt?“

✅ Besser so:

Hoppla, diese Seite ist verschwunden
Der Link ist vielleicht abgelaufen oder die Seite wurde verschoben

Menschenverständlich – kein Technikjargon.

2. Hauptnavigation oder Link zur Startseite

Der minimale Rettungsweg. Nutzer wissen, wo sie sicher landen.

<Link href="/" className="text-blue-600">Zur Startseite</Link>

3. Suchfeld

Vielleicht war die URL falsch getippt oder der Link abgelaufen. Ein Suchfeld hilft beim Selbstfinden.

Spotify setzt auf ein großes Suchfeld mit dem Text „Search for what you’re looking for“. Klar und direkt.

4. Empfohlene Inhalte oder beliebte Seiten

Der Nutzer ist da – geben Sie ihm etwas zu sehen:

  • Blog → neueste Artikel empfehlen
  • E-Commerce → beliebte Produkte
  • SaaS → Einstieg in Kernfunktionen

Netflix empfiehlt auf der 404-Seite beliebte Serien – viele bleiben und vergessen, warum sie kamen.

5. Markenkonsistenz

Logo, Farben, Schrift – wie auf dem Rest der Website.

Fehlerseiten sind Teil der Markenerfahrung. Eine langweilige weiße Seite wirkt unseriös.

Designstrategien: Peinlichkeit entschärfen

Neben Funktion zählt die Atmosphäre.

Humor entschärft

Figma zeigt auf der 404-Seite eine Animation: ein UI-Element huscht über den Bildschirm, unerreichbar. Text: „Hmm, we can’t find that page.“

Leicht und sympathisch – Nutzer denken nicht „Alles kaputt“, sondern schmunzeln.

Aber nicht übertreiben. Tech-Firmen dürfen spielen; Finanz- oder Medizin-Websites wirken damit unprofessionell.

Entschädigung (E-Commerce)

Manche Shops bieten auf der 404-Seite einen Gutschein: „Seite nicht gefunden – hier 10 % Rabatt.“

Enttäuschung wird Kaufimpuls – manchmal bestellen Nutzer trotzdem.

Mobile nicht vergessen

Etwa 40 % des Traffics kommt vom Handy – Fehlerseiten müssen passen.

  • Buttons groß genug (mindestens 44×44 px)
  • Wenig Text auf kleinem Display
  • Wichtigste Links oben, sofort sichtbar

Ich habe 404-Seiten gesehen, die am Desktop schön sind – am Handy winzige Buttons, dreimal tippen für „Zur Startseite“. UX ruiniert.

Praxisbeispiele: Gut und schlecht

Schlechtes Beispiel – manche Behörden-Websites:

  • Weiß-Schwarz, „Error 404 Not Found“
  • Keine Links
  • Kein Suchfeld
  • Kein Logo

100 % Absprung.

Gutes Beispiel – Airbnb:

  • Große Überschrift: „We can’t seem to find the page you’re looking for“
  • Suchfeld: „Try searching for hotels in Paris“
  • Links: Homes, Experiences, Online Experiences
  • Airbnb-Farben und -Typografie

Nutzer bleiben oft wegen der Empfehlungen – auch ohne Zielseite.

Zahlen sprechen lassen

A/B-Test auf meinem Blog:

Version A (Standard-404):

  • Absprungrate: 78 %
  • Durchschnittliche Verweildauer: 3 Sekunden

Version B (benutzerdefinierte 404 mit Suche und Artikelempfehlungen):

  • Absprungrate: 42 %
  • Durchschnittliche Verweildauer: 35 Sekunden

Absprungrate halbiert! 20 % klickten empfohlene Artikel und lasen weiter.

Gleicher Fehler „Seite nicht existiert“ – ein Design vertreibt, das andere hält.

Häufige Probleme und Erfahrungen aus der Praxis

Viele Projekte, viele Fallen. Hier die häufigsten Probleme und Lösungen.

Problem 1: notFound() liefert 200 statt 404

Symptom:

notFound() wurde aufgerufen, 404-Seite erscheint – aber in den DevTools steht HTTP 200. Google indexiert als normale Seite, SEO leidet.

Ursache:

Streaming hat begonnen, Status ist auf 200 gesperrt. Sobald JSX fließt, ist es zu spät.

Lösung:

notFound() vor jedem JSX-Return aufrufen.

// ❌ Falsch: JSX läuft schon
export default async function Page({ params }) {
  const data = await fetchData(params.id)
  return <div>{!data ? notFound() : <Content data={data} />}</div>
}

// ✅ Richtig: Erst prüfen, dann rendern
export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound()  // Sofort aufrufen
  }

  return <Content data={data} />
}

Merksatz: Erst prüfen, dann notFound(), dann rendern.

Problem 2: Styles in global-error.tsx greifen nicht

Symptom:

Tailwind oder CSS-Module in global-error.tsx importiert – auf der Seite nichts sichtbar.

Ursache:

Next.js ignoriert CSS-Imports in global-error.tsx. Bekannte Einschränkung.

Lösung:

Nur Inline-Styles oder <style>-Tags.

// ❌ Falsch: Import wirkt nicht
import './styles.css'  // Wird ignoriert

export default function GlobalError() {
  return <div className="bg-blue-500">Fehler</div>  // Tailwind auch nicht
}

// ✅ Richtig: style-Tag
export default function GlobalError() {
  return (
    <html>
      <body>
        <style>{`
          .error-container {
            background: #3b82f6;
            color: white;
            padding: 2rem;
          }
        `}</style>
        <div className="error-container">Fehler</div>
      </body>
    </html>
  )
}

Etwas roh, aber funktional. Styles oft als String-Konstante auslagern.

Problem 3: not-found.tsx in verschachtelten Routen greift nicht

Symptom:

app/blog/[slug]/not-found.tsx existiert, aber /blog/nicht-existierender-artikel zeigt die globale 404.

Ursache:

Meist zwei Dinge:

  1. Falsche Dateiposition
  2. Kein notFound() in page.tsx

Lösung:

Struktur prüfen:

app/
├── not-found.tsx          ← Globale 404
└── blog/
    └── [slug]/
        ├── page.tsx       ← notFound() hier aufrufen
        └── not-found.tsx  ← Blog-spezifische 404

In page.tsx aktiv auslösen:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()  // Löst not-found.tsx auf derselben Ebene aus
  }

  return <article>{post.title}</article>
}

Komplett unbekannte Routen wie /asdfghjkl nutzen app/not-found.tsx.

Verschachtelte not-found.tsx greift nur, wenn die zugehörige page.tsx notFound() aufruft.

Problem 4: error.tsx fängt manche Fehler nicht

Symptom:

Datenbankverbindung fehlgeschlagen – error.tsx erscheint nicht, weißer Bildschirm oder Root-Fehlerseite.

Ursache:

error.tsx fängt nur Fehler auf derselben Ebene und darunter. Fehler im eigenen layout.tsx nicht.

Außerdem: notFound() überspringt error.tsx und geht direkt zu not-found.tsx.

Lösung:

Bei Layout-Verdacht error.tsx auf übergeordneter oder Root-Ebene:

app/
├── error.tsx              ← Fehler in Root-Layout-Unterkomponenten
├── global-error.tsx       ← Fehler im Root-Layout selbst
└── dashboard/
    ├── layout.tsx         ← Fehler hier: unteres error.tsx hilft nicht
    └── error.tsx          ← Nur page.tsx und Unterrouten

Layout-Fehler wirklich abfangen: global-error.tsx.

Problem 5: In Produktion keine Fehlerdetails

Symptom:

Entwicklung: detaillierte Meldungen. Produktion: error.message nur „Application error“.

Ursache:

Next.js-Sicherheitsmechanismus gegen Informationslecks.

Lösung:

Mit error.digest in Server-Logs nachschlagen:

'use client'

export default function Error({ error }) {
  return (
    <div>
      <p>Fehler: {error.message}</p>
      <p className="text-xs text-gray-400">
        Fehler-ID: {error.digest}  {/* Dem Nutzer anzeigen */}
      </p>
    </div>
  )
}

Screenshot mit ID → in Vercel-, Sentry- oder Datadog-Logs den vollständigen Stack finden.

Oder in error.tsx per useEffect direkt an Monitoring senden – ohne auf Nutzer-Feedback zu warten.

Fazit

Kurz zusammengefasst – drei Ebenen in Next.js:

  • not-found.tsx → 404, Seite existiert nicht
  • error.tsx → Laufzeitfehler
  • global-error.tsx → Root-Layout-Fallback

Technisch ist das überschaubar. Die Herausforderung ist Design. Eine gute Fehlerseite kann die Absprungrate von 78 % auf 42 % senken – gemessen, nicht geraten.

Suchfeld, ein paar Empfehlungslinks, ein menschlicher Satz – oft reicht das.

Schauen Sie in Ihr Next.js-Projekt: Noch Standard-Fehlerseiten? Eine halbe Stunde Aufwand – Nutzer werden es merken.

Fragen? Schreiben Sie gerne. Wenn der Artikel geholfen hat, teilen Sie ihn mit anderen Entwicklern.

Benutzerdefinierte Next.js-404-Seite erstellen

Schritt-für-Schritt-Anleitung für eine benutzerdefinierte 404-Fehlerseite im Next.js App Router – mit Suchfeld und Empfehlungslinks

  1. 1

    Step 1: not-found.tsx erstellen

    Erstellen Sie im app-Verzeichnis eine not-found.tsx-Datei als globale 404-Seite
  2. 2

    Step 2: Basis-UI-Komponente hinzufügen

    Importieren Sie die Next.js Link-Komponente und erstellen Sie eine Grundoberfläche mit Fehlermeldung und Zurück-zur-Startseite-Button
  3. 3

    Step 3: 'use client'-Direktive hinzufügen

    Fügen Sie am Dateianfang 'use client' hinzu, wenn Sie State-Management oder Interaktionen (z. B. Suchfeld) benötigen
  4. 4

    Step 4: Suchfunktion implementieren

    Verwenden Sie useState für die Sucheingabe und useRouter für die Suchnavigation
  5. 5

    Step 5: Beliebte Links hinzufügen

    Erstellen Sie ein Array mit empfohlenen Seitenlinks und rendern Sie die Navigation mit der Link-Komponente
  6. 6

    Step 6: Styling optimieren

    Gestalten Sie die Seite mit Tailwind CSS oder einer anderen Styling-Lösung und halten Sie die Markenidentität konsistent
  7. 7

    Step 7: 404 in Seitenkomponenten auslösen

    Rufen Sie in page.tsx dynamischer Routen notFound() auf, wenn die angeforderten Daten nicht existieren
  8. 8

    Step 8: Testen und validieren

    Rufen Sie einen nicht existierenden Pfad auf und prüfen Sie in den Browser-DevTools, ob der HTTP-Status 404 ist

FAQ

Was ist der Unterschied zwischen not-found.tsx, error.tsx und global-error.tsx in Next.js?
not-found.tsx behandelt 404-Fehler (Seite nicht gefunden); error.tsx behandelt Laufzeitfehler wie fehlgeschlagene Datenladung; global-error.tsx ist das letzte Sicherheitsnetz und fängt sogar Fehler im Root-Layout ab. Zusammen bilden sie ein dreistufiges Fehlerbehandlungssystem.
Warum ist der HTTP-Status nach notFound() 200 statt 404?
Weil notFound() nach dem JSX-Return aufgerufen wurde – zu diesem Zeitpunkt hat die Streaming-Antwort bereits begonnen und der Status ist auf 200 gesperrt. Rufen Sie notFound() vor jedem JSX-Return auf: erst Daten prüfen, dann rendern.
Warum kann global-error.tsx kein Tailwind CSS oder CSS-Dateien importieren?
Next.js ignoriert CSS-Imports in global-error.tsx, weil diese Datei das Root-Layout vollständig ersetzt. Sie können nur Inline-Styles oder style-Tags verwenden – das ist eine Framework-Einschränkung.
Wie kann ich nachverfolgen, welche nicht existierenden Seiten Nutzer aufrufen?
Verwenden Sie in not-found.tsx useEffect und usePathname, um 404-Pfade an Google Analytics oder Ihren eigenen Server zu senden. Aus diesen Daten erkennen Sie fehlende Inhalte oder alte Seiten, die 301-Weiterleitungen brauchen.
Welche Elemente sollte eine benutzerdefinierte 404-Seite enthalten, um die Absprungrate zu senken?
Eine gute 404-Seite enthält: 1) eine klare, freundliche Fehlermeldung (ohne Fachjargon); 2) Links zur Startseite oder Hauptnavigation; 3) ein Suchfeld; 4) empfohlene Inhalte oder beliebte Seiten; 5) konsistente Markenelemente. Damit lässt sich die Absprungrate von 78 % auf 42 % senken.

15 Min. Lesezeit · Veröffentlicht am: 5. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog