Tous les articles
·Ingénierie·9 min

Gestion des erreurs dans Next.js App Router : error.tsx, global-error et notFound

Gestion des erreurs Next.js App Router : error.tsx, global-error et notFound expliqués avec du code pour ne plus jamais afficher un écran blanc.

Un formulaire qui plante et l'utilisateur se retrouve devant un écran blanc : c'est le pire scénario en production, et pourtant il reste fréquent. La gestion des erreurs Next.js n'est pas une couche qu'on ajoute à la fin, c'est une décision d'architecture qu'on prend dès la première route. L'App Router fournit un système d'Error Boundaries basé sur des fichiers, et bien utilisé, il transforme un crash silencieux en une UI de repli propre et récupérable.

Le problème, c'est que les conventions se ressemblent et qu'on les confond vite. error.tsx, global-error.tsx, not-found.tsx, notFound() : chacune couvre un cas précis, et les empiler au hasard produit des comportements que personne ne comprend six mois plus tard.

Cet article pose les règles claires : quand une erreur doit être lancée, quand elle doit être renvoyée, où placer chaque fichier, et comment garder un message lisible en dev sans jamais fuiter une stack trace côté client en production.

Écran de code illustrant la gestion des erreurs dans une application Next.js
Une bonne stratégie d'erreurs se conçoit route par route, pas globalement à la fin du projet.

Gestion des erreurs Next.js : attendues ou inattendues

Avant d'écrire un seul fichier error.tsx, il faut trancher une question : cette erreur est-elle attendue ou non ? La réponse conditionne toute la stratégie. Une erreur attendue, c'est un email déjà pris, un paiement refusé, un formulaire invalide. Elle fait partie du flux normal et ne doit jamais remonter dans une Error Boundary. On la traite comme une valeur de retour, pas comme une exception.

Une erreur inattendue, c'est une base de données injoignable, un undefined qui traîne, un appel réseau qui casse. Là, on veut que ça remonte jusqu'à l'Error Boundary la plus proche et qu'une UI de repli s'affiche. Le réflexe classique, hérité du try/catch partout, consiste à tout intercepter et à renvoyer un objet d'erreur générique. C'est une erreur : vous perdez le mécanisme d'Error Boundary et vous vous retrouvez à réinventer un système de propagation à la main.

La règle tient en une phrase : les erreurs attendues se renvoient (return { error }), les erreurs inattendues se lancent (throw). Toute la mécanique de fichiers de Next.js repose sur cette distinction. Si vous validez vos entrées avec Zod, la plupart de vos erreurs attendues sont déjà typées côté serveur avant même d'atteindre votre logique métier.

error.tsx : l'Error Boundary de segment

error.tsx est le cœur de la gestion des erreurs Next.js dans l'App Router. Placé dans un dossier de route, il crée automatiquement une Error Boundary React qui enveloppe le page.tsx du segment, ses composants enfants et les layouts imbriqués en dessous. Point crucial : il n'enveloppe pas le layout du même segment. Une erreur dans app/dashboard/layout.tsx ne sera donc pas attrapée par app/dashboard/error.tsx, mais par l'Error Boundary du parent. Cette subtilité explique la moitié des bugs « mon error.tsx ne se déclenche jamais ».

Ce fichier doit obligatoirement être un Client Component. Il reçoit deux props : error, l'instance capturée, et reset, une fonction pour retenter le rendu. Placer un error.tsx par section fonctionnelle permet un repli granulaire : une erreur dans le widget de stats ne fait pas tomber toute la page, seulement la portion concernée.

'use client' // Obligatoire : une Error Boundary React vit côté client

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  // digest est l'identifiant serveur à croiser avec vos logs en prod
  return (
    <div role="alert">
      <p>Ce module n'a pas pu se charger.</p>
      <button onClick={() => reset()}>Réessayer</button>
    </div>
  )
}

reset() : récupérer sans recharger la page

La prop reset est ce qui distingue une vraie stratégie d'erreurs d'un simple message d'excuse. Appeler reset() demande à React de retenter le rendu du segment qui a échoué, sans rechargement complet du navigateur. Si l'erreur était transitoire (un timeout réseau, un cold start de base de données) le second essai passe et l'utilisateur reprend là où il était.

Le piège, c'est que reset() seul ne suffit pas toujours. Si les données à l'origine de l'erreur viennent d'un Server Component, un simple re-render côté client rejoue le même état et échoue de nouveau. La solution consiste à combiner reset() avec un router.refresh() pour forcer Next.js à réexécuter la partie serveur. On enveloppe généralement les deux dans une transition pour garder l'interface réactive pendant la nouvelle tentative.

Cette logique de retry manuel se marie bien avec une couche de data-fetching côté client. Si vous utilisez déjà TanStack Query pour l'hydratation, le retry automatique de la librairie couvre les erreurs de requêtes, tandis que error.tsx reste le filet de sécurité pour tout ce qui casse en dehors du cache. Les deux mécanismes ne s'opposent pas, ils opèrent à des niveaux différents de la pile.

global-error.tsx : le dernier rempart de l'application

Que se passe-t-il quand c'est le layout racine lui-même qui plante ? Aucun error.tsx ne peut l'attraper, puisqu'ils vivent tous à l'intérieur de ce layout. C'est le rôle de global-error.tsx, placé à la racine du dossier app. Il remplace intégralement le layout racine quand il s'active, ce qui impose une contrainte souvent oubliée : il doit définir ses propres balises <html> et <body>, puisqu'il se substitue à celles du layout.

Deuxième particularité, et source de confusion garantie : global-error.tsx ne s'active qu'en production. En développement, Next.js affiche son overlay d'erreur détaillé à la place, ce qui fait croire que le fichier ne fonctionne pas. Il fonctionne, mais vous ne le verrez qu'après un next build && next start.

'use client'

export default function GlobalError({ reset }: { reset: () => void }) {
  // Doit porter <html> et <body> : il remplace le layout racine entier
  return (
    <html lang="fr">
      <body>
        <h1>Une erreur critique est survenue</h1>
        <button onClick={() => reset()}>Recharger</button>
      </body>
    </html>
  )
}

En pratique, global-error.tsx sert de garde-fou ultime : il ne doit quasiment jamais s'afficher. S'il se déclenche souvent, c'est le signe d'un layout racine trop chargé en logique. Gardez-le minimal et sans dépendances externes, sinon il peut planter à son tour.

notFound() et not-found.tsx : un 404 propre, pas un crash

Une ressource absente n'est pas une erreur applicative, c'est un état légitime. Next.js le formalise avec la fonction notFound(), qui interrompt le rendu et déclenche le fichier not-found.tsx le plus proche. Vous l'appelez typiquement dans un Server Component après une requête qui ne renvoie rien : article introuvable, utilisateur supprimé, slug inexistant.

import { notFound } from 'next/navigation'

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await getArticle(params.slug)
  // notFound() interrompt le rendu, attrapé par le not-found.tsx le plus proche
  if (!article) notFound()

  return <Article data={article} />
}

L'intérêt est double. Côté UX, l'utilisateur voit une vraie page 404 contextualisée plutôt qu'un écran d'erreur générique. Côté SEO, Next.js renvoie bien un statut HTTP 404, ce qui évite que Google indexe des pages vides. Depuis les versions récentes, global-not-found.tsx permet en plus de définir une page 404 globale pour toutes les URLs non matchées, là où not-found.tsx reste scopé à un segment. Distinguez bien les deux : le premier gère les routes qui n'existent pas du tout, le second gère les ressources absentes à l'intérieur d'une route qui, elle, existe.

Erreurs Next.js dans les Server Actions : renvoyer plutôt que lancer

Les Server Actions changent la donne pour les erreurs de formulaire. Ici, lancer une exception est presque toujours le mauvais choix : une erreur attendue comme « mot de passe trop court » n'a rien à faire dans une Error Boundary. On la renvoie comme valeur, et le composant client la lit via useActionState pour afficher le message au bon endroit, à côté du champ concerné.

'use server'

export async function createUser(prev: State, formData: FormData) {
  const parsed = schema.safeParse(Object.fromEntries(formData))
  // Erreur attendue : on renvoie, on ne lance pas
  if (!parsed.success) return { error: 'Email invalide' }

  await db.user.create({ data: parsed.data })
  return { success: true }
}

Cette approche s'appuie directement sur la validation en amont. En couplant vos actions à Zod côté serveur, vous transformez la quasi-totalité de vos erreurs métier en valeurs de retour typées, propres à afficher. Réservez le throw aux vrais imprévus (la base qui tombe, l'API tierce qui timeout) pour qu'ils remontent jusqu'à error.tsx. Le côté client, lui, s'articule autour des nouveaux hooks React 19 qui rendent cette gestion d'état d'erreur beaucoup moins verbeuse qu'avant.

Observabilité : capturer la stack, pas la montrer

En production, l'objet error reçu par vos boundaries est volontairement expurgé : le message est remplacé par un générique et seule une propriété digest subsiste. C'est une mesure de sécurité pour ne pas fuiter d'informations sensibles au client. Ce digest est un hash déterministe que Next.js écrit aussi dans vos logs serveur. En le croisant, vous retrouvez la stack trace complète sans jamais l'exposer au navigateur.

Concrètement, cela signifie qu'un error.tsx sans instrumentation ne vous apprend rien : vous voyez que quelque chose a cassé, mais pas quoi. Branchez un outil comme Sentry dans un useEffect du composant d'erreur pour remonter chaque incident avec son digest, son URL et le contexte utilisateur. Vous corrélerez ensuite ces remontées client avec vos logs serveur pour reconstituer le fil complet. Sans cette boucle, vous découvrirez vos erreurs de production par les plaintes des utilisateurs, ce qui est déjà trop tard.

En pratique

Une gestion des erreurs Next.js solide se construit par couches, pas d'un bloc. Commencez par la frontière attendu/inattendu dans chaque Server Action, ajoutez un error.tsx par section fonctionnelle plutôt qu'un seul à la racine, gérez les ressources absentes avec notFound(), et gardez global-error.tsx comme dernier recours minimal. Chaque niveau attrape ce que le niveau en dessous n'a pas su gérer, et l'utilisateur ne voit jamais un écran blanc.

Le vrai gain apparaît quand cette stratégie se combine avec le reste de votre pile. La validation en entrée réduit le volume d'erreurs inattendues, l'observabilité vous les fait découvrir avant vos clients, et les fichiers de repli garantissent que même le pire cas reste récupérable. C'est moins spectaculaire qu'une nouvelle fonctionnalité, mais c'est ce qui distingue un site de démo d'une application qui tient en production. Pour aller plus loin sur la robustesse côté données, notre article sur le caching dans l'App Router montre comment éviter qu'une revalidation ratée ne devienne une erreur visible.

Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit ou d'un renfort tech ? Parlons-en.

Sources

  1. Getting Started: Error Handling, Next.js
  2. File-system conventions: error.js, Next.js
  3. File-system conventions: not-found.js, Next.js