Cours Next.js · Module 6: Formulaires et Server Actions

Les composants error.tsx pour la gestion des erreurs

11 min de lecture
Dans cette leçon9

Lors de la construction d'applications Next.js, un aspect important est de gérer correctement les scénarios d'échec. Le framework Next.js offre un mécanisme simple mais puissant qui permet d'isoler et de gérer facilement les erreurs dans une application - les composants error.tsx. Dans ce module, nous verrons comment utiliser efficacement ces composants pour améliorer la résilience et l'expérience utilisateur de notre application.

Qu'est-ce que le composant error.tsx ?

Le composant error.tsx est un fichier spécial dans la structure d'une application Next.js qui agit comme une « frontière d'erreur » (error boundary) pour un segment donné de l'application. Lorsqu'une erreur survient dans ce segment ou ses enfants :

  1. Next.js capture automatiquement l'erreur
  2. Rend le composant error.tsx à la place du composant qui a produit l'erreur
  3. Isole l'erreur au segment concerné, permettant au reste de l'application de continuer à fonctionner normalement

Comment fonctionne error.tsx dans l'App Router de Next.js ?

Dans l'architecture App Router, nous pouvons placer des composants error.tsx à n'importe quel niveau de la structure de répertoires, ce qui permet de définir très précisément les frontières d'erreur.

Implémentation de base

Voici à quoi ressemble un composant error.tsx simple :

1'use client'; // Doit être un composant client
2
3import { useEffect } from 'react';
4
5interface ErrorComponentProps {
6  error: Error & { digest?: string };
7  reset: () => void;
8}
9
10export default function Error({ error, reset }: ErrorComponentProps) {
11  useEffect(() => {
12    // Signaler éventuellement l'erreur à un service de monitoring
13    console.error("Une erreur inattendue s'est produite :", error);
14  }, [error]);
15
16  return (
17    <div className="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md">
18      <h2 className="text-xl font-bold text-red-600 mb-4">Quelque chose a mal tourné !</h2>
19      <p className="text-gray-700 mb-4">
20        Désolé, une erreur inattendue s'est produite. Notre équipe a été prévenue.
21      </p>
22      <button
23        onClick={reset}
24        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
25      >
26        Réessayer
27      </button>
28    </div>
29  );
30}

Éléments clés :

  1. Marqueur 'use client' - le composant error.tsx doit être marqué comme composant client, même si le reste de l'application utilise le rendu côté serveur.

  2. Props :

    • error : objet d'erreur contenant des informations sur ce qui a mal tourné
    • reset : fonction qui permet de réessayer le rendu du composant ayant produit l'erreur
  3. Fonction reset() : permet aux utilisateurs de réessayer une opération sans recharger toute la page.

Stratégies de gestion des erreurs avec error.tsx

1. Gestion hiérarchique des erreurs

Next.js te permet de créer des composants error.tsx à différents niveaux de la structure de répertoires, offrant un contrôle précis sur la manière dont les erreurs sont gérées dans différentes parties de l'application.

1app/
2├── error.tsx          # Solution de repli globale pour les erreurs de toute l'application
3├── layout.tsx
4├── page.tsx
5├── dashboard/
6│   ├── error.tsx      # Gestion des erreurs pour tout le tableau de bord
7│   ├── layout.tsx
8│   ├── page.tsx
9│   └── settings/
10│       ├── error.tsx  # Spécifique à la section des paramètres
11│       └── page.tsx
12└── profile/
13    ├── error.tsx      # Spécifique au profil utilisateur
14    └── page.tsx

2. Messages d'erreur détaillés en environnement de développement

Nous pouvons adapter le composant error.tsx pour afficher des informations d'erreur plus détaillées en environnement de développement :

1'use client';
2
3export default function Error({ error, reset }) {
4  const isDevelopment = process.env.NODE_ENV === 'development';
5
6  return (
7    <div>
8      <h2>Quelque chose a mal tourné !</h2>
9
10      {isDevelopment && (
11        <div className="bg-gray-100 p-4 rounded my-4">
12          <h3 className="font-bold">Détails de l'erreur (mode dev uniquement) :</h3>
13          <p className="font-mono">{error.message}</p>
14          <pre className="overflow-auto text-sm mt-2">{error.stack}</pre>
15        </div>
16      )}
17
18      <button onClick={reset}>Réessayer</button>
19    </div>
20  );
21}

3. Différents types d'erreurs, différents messages

Nous pouvons personnaliser l'apparence et les messages selon le type d'erreur :

1'use client';
2
3import { useEffect } from 'react';
4
5export default function Error({ error, reset }) {
6  // Journaliser l'erreur dans le système de monitoring
7  useEffect(() => {
8    console.error(error);
9    // logErrorToService(error);
10  }, [error]);
11
12  // Déterminer le type d'erreur d'après le message ou d'autres propriétés
13  const is404 = error.message?.includes('not found') || error.statusCode === 404;
14  const isAuthentication = error.message?.includes('unauthorized') || error.statusCode === 401;
15
16  return (
17    <div className="p-6 mx-auto max-w-md">
18      {is404 ? (
19        <>
20          <h2 className="text-xl font-bold">Ressource introuvable</h2>
21          <p>Désolé, la ressource demandée n'existe pas.</p>
22        </>
23      ) : isAuthentication ? (
24        <>
25          <h2 className="text-xl font-bold">Authentification requise</h2>
26          <p>Tu dois te connecter pour accéder à cette ressource.</p>
27          <button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
28            Se connecter
29          </button>
30        </>
31      ) : (
32        <>
33          <h2 className="text-xl font-bold text-red-600">Erreur inattendue</h2>
34          <p>Désolé, un problème inattendu s'est produit.</p>
35        </>
36      )}
37
38      <button
39        onClick={reset}
40        className="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
41      >
42        Réessayer
43      </button>
44    </div>
45  );
46}

Intégration avec les systèmes de monitoring des erreurs

Dans les applications en production, la collecte d'informations sur les erreurs est essentielle. Les composants error.tsx peuvent être intégrés à des systèmes externes de monitoring des erreurs comme Sentry, LogRocket ou New Relic :

1'use client';
2
3import { useEffect } from 'react';
4import * as Sentry from '@sentry/nextjs';
5
6export default function Error({ error, reset }) {
7  useEffect(() => {
8    // Envoyer les informations d'erreur à Sentry
9    Sentry.captureException(error);
10  }, [error]);
11
12  return (
13    <div>
14      <h2>Quelque chose a mal tourné !</h2>
15      <p>Notre équipe a été prévenue du problème.</p>
16      <button onClick={reset}>Réessayer</button>
17    </div>
18  );
19}

Gérer les erreurs d'API et de fetch

Les erreurs lors de la récupération de données dans les composants Next.js peuvent être gérées avec error.tsx, mais il faut garder à l'esprit quelques nuances :

Dans les composants serveur

1// app/products/page.tsx
2async function getProducts() {
3  const res = await fetch('https://api.example.com/products');
4
5  if (!res.ok) {
6    // Cette erreur sera capturée par le error.tsx le plus proche
7    throw new Error('Échec de la récupération des produits');
8  }
9
10  return res.json();
11}
12
13export default async function ProductsPage() {
14  const products = await getProducts();
15
16  return (
17    <div>
18      <h1>Nos produits</h1>
19      <ul>
20        {products.map(product => (
21          <li key={product.id}>{product.name}</li>
22        ))}
23      </ul>
24    </div>
25  );
26}

Dans les composants client

1'use client';
2
3import { useState, useEffect } from 'react';
4import { useRouter } from 'next/navigation';
5
6export default function ClientProductsPage() {
7  const [products, setProducts] = useState([]);
8  const [error, setError] = useState(null);
9  const router = useRouter();
10
11  useEffect(() => {
12    async function fetchProducts() {
13      try {
14        const res = await fetch('/api/products');
15
16        if (!res.ok) {
17          throw new Error('Erreur de récupération des données');
18        }
19
20        const data = await res.json();
21        setProducts(data);
22      } catch (err) {
23        setError(err.message);
24        // Facultatif - nous pouvons invoquer manuellement le error.tsx le plus proche
25        // throw err; // Cela fera utiliser à Next.js le error.tsx le plus proche
26      }
27    }
28
29    fetchProducts();
30  }, []);
31
32  if (error) {
33    return (
34      <div className="p-4 bg-red-50 border border-red-200 rounded">
35        <h2 className="text-red-600 font-bold">Une erreur s'est produite</h2>
36        <p>{error}</p>
37        <button onClick={() => router.refresh()}>Rafraîchir la page</button>
38      </div>
39    );
40  }
41
42  return (
43    <div>
44      <h1>Produits (Client)</h1>
45      {/* ... */}
46    </div>
47  );
48}

Bonnes pratiques pour travailler avec error.tsx

  1. Granularité appropriée - utilise error.tsx à des niveaux qui offrent une isolation d'erreur pertinente, sans créer trop de composants.

  2. Messages utiles - fournis des messages conviviaux et des instructions claires pour les étapes suivantes.

  3. Options de récupération - propose des moyens de se remettre de la situation, comme un bouton « Réessayer » (via la fonction reset).

  4. Suivi et monitoring - journalise toujours les erreurs dans les systèmes de monitoring pour pouvoir les suivre et les résoudre.

  5. Tests - teste divers scénarios d'erreur pour t'assurer que tes frontières d'erreur réagissent correctement.

  6. Accessibilité - assure-toi que les messages d'erreur sont aussi accessibles aux utilisateurs de technologies d'assistance.

Capturer les erreurs de layout.tsx

Les frontières d'erreur de error.tsx ne capturent pas les erreurs provenant des composants layout.tsx du même segment. Pour gérer les erreurs des layouts, tu dois créer un composant error.tsx dans le segment parent.

1app/
2├── dashboard/
3│   ├── layout.tsx  // Les erreurs ici ne sont PAS capturées par error.tsx au même niveau
4│   ├── error.tsx   // Cela ne capturera pas les erreurs de layout.tsx
5│   └── page.tsx

Structure correcte pour capturer les erreurs de layout.tsx :

1app/
2├── error.tsx     // Cela capturera les erreurs de dashboard/layout.tsx
3├── dashboard/
4│   ├── layout.tsx
5│   └── page.tsx

Exemple d'implémentation complète

Voici un exemple de composant error.tsx plus avancé qui gère divers types d'erreurs, des options de récupération et l'intégration avec un système de monitoring externe :

1'use client';
2
3import { useEffect, useState } from 'react';
4import Link from 'next/link';
5import { useRouter } from 'next/navigation';
6
7// Types d'erreurs que nous pouvons gérer spécifiquement
8enum ErrorType {
9  NOT_FOUND = 'NOT_FOUND',
10  UNAUTHORIZED = 'UNAUTHORIZED',
11  FORBIDDEN = 'FORBIDDEN',
12  TIMEOUT = 'TIMEOUT',
13  SERVER_ERROR = 'SERVER_ERROR',
14  UNKNOWN = 'UNKNOWN'
15}
16
17interface ErrorComponentProps {
18  error: Error & {
19    digest?: string;
20    statusCode?: number;
21  };
22  reset: () => void;
23}
24
25export default function Error({ error, reset }: ErrorComponentProps) {
26  const router = useRouter();
27  const [errorType, setErrorType] = useState<ErrorType>(ErrorType.UNKNOWN);
28  const [isReporting, setIsReporting] = useState(false);
29
30  // Déterminer le type d'erreur
31  useEffect(() => {
32    if (error.message?.toLowerCase().includes('not found') || error.statusCode === 404) {
33      setErrorType(ErrorType.NOT_FOUND);
34    } else if (error.message?.toLowerCase().includes('unauthorized') || error.statusCode === 401) {
35      setErrorType(ErrorType.UNAUTHORIZED);
36    } else if (error.message?.toLowerCase().includes('forbidden') || error.statusCode === 403) {
37      setErrorType(ErrorType.FORBIDDEN);
38    } else if (error.message?.toLowerCase().includes('timeout') || error.message?.includes('timed out')) {
39      setErrorType(ErrorType.TIMEOUT);
40    } else if (error.statusCode >= 500 && error.statusCode < 600) {
41      setErrorType(ErrorType.SERVER_ERROR);
42    }
43  }, [error]);
44
45  // Signalement des erreurs
46  useEffect(() => {
47    // Journalisation dans la console en dev
48    console.error("Erreur de l'application :", error);
49
50    // Dans une vraie application, nous enverrions ceci à un service externe
51    async function reportError() {
52      try {
53        setIsReporting(true);
54        // Exemple d'implémentation
55        // await fetch('/api/error-reporting', {
56        //   method: 'POST',
57        //   headers: { 'Content-Type': 'application/json' },
58        //   body: JSON.stringify({
59        //     message: error.message,
60        //     stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
61        //     url: window.location.href,
62        //     type: errorType,
63        //     timestamp: new Date().toISOString()
64        //   })
65        // });
66        await new Promise(resolve => setTimeout(resolve, 1000)); // Simulation
67      } catch (e) {
68        console.error("Échec du signalement de l'erreur :", e);
69      } finally {
70        setIsReporting(false);
71      }
72    }
73
74    reportError();
75  }, [error, errorType]);
76
77  // Afficher différents messages selon le type d'erreur
78  function renderErrorContent() {
79    switch (errorType) {
80      case ErrorType.NOT_FOUND:
81        return (
82          <>
83            <h2 className="text-2xl font-bold text-gray-800 mb-3">Ressource introuvable</h2>
84            <p className="text-gray-600 mb-4">
85              La page que tu cherches n'existe pas ou a été déplacée.
86            </p>
87            <div className="flex space-x-4">
88              <button
89                onClick={() => router.back()}
90                className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
91              >
92                Retour
93              </button>
94              <Link href="/" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
95                Page d'accueil
96              </Link>
97            </div>
98          </>
99        );
100
101      case ErrorType.UNAUTHORIZED:
102        return (
103          <>
104            <h2 className="text-2xl font-bold text-gray-800 mb-3">Connexion requise</h2>
105            <p className="text-gray-600 mb-4">
106              Tu dois te connecter pour accéder à cette page.
107            </p>
108            <Link
109              href="/login"
110              className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
111            >
112              Se connecter
113            </Link>
114          </>
115        );
116
117      case ErrorType.FORBIDDEN:
118        return (
119          <>
120            <h2 className="text-2xl font-bold text-gray-800 mb-3">Accès refusé</h2>
121            <p className="text-gray-600 mb-4">
122              Tu n'as pas la permission d'accéder à cette page ou ressource.
123            </p>
124            <button
125              onClick={() => router.back()}
126              className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
127            >
128              Retour
129            </button>
130          </>
131        );
132
133      case ErrorType.TIMEOUT:
134        return (
135          <>
136            <h2 className="text-2xl font-bold text-gray-800 mb-3">Délai de requête dépassé</h2>
137            <p className="text-gray-600 mb-4">
138              L'opération a pris trop de temps. Vérifie ta connexion internet et réessaie.
139            </p>
140            <button
141              onClick={reset}
142              className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
143            >
144              Réessayer
145            </button>
146          </>
147        );
148
149      case ErrorType.SERVER_ERROR:
150        return (
151          <>
152            <h2 className="text-2xl font-bold text-gray-800 mb-3">Erreur serveur</h2>
153            <p className="text-gray-600 mb-4">
154              Désolé, une erreur s'est produite côté serveur. Notre équipe a été prévenue.
155            </p>
156            <button
157              onClick={reset}
158              className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
159            >
160              Réessayer
161            </button>
162          </>
163        );
164
165      default:
166        return (
167          <>
168            <h2 className="text-2xl font-bold text-gray-800 mb-3">Quelque chose a mal tourné</h2>
169            <p className="text-gray-600 mb-4">
170              Désolé, une erreur inattendue s'est produite. {isReporting ? 'Signalement du problème...' : 'Notre équipe a été prévenue.'}
171            </p>
172            <div className="flex space-x-4">
173              <button
174                onClick={reset}
175                className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
176              >
177                Réessayer
178              </button>
179              <button
180                onClick={() => router.refresh()}
181                className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
182              >
183                Rafraîchir la page
184              </button>
185            </div>
186          </>
187        );
188    }
189  }
190
191  // Rendu pour le mode développement - informations d'erreur supplémentaires
192  function renderDevModeInfo() {
193    if (process.env.NODE_ENV !== 'development') return null;
194
195    return (
196      <div className="mt-8 p-4 bg-gray-100 rounded-lg border border-gray-300">
197        <h3 className="text-sm font-bold uppercase text-gray-500 mb-2">
198          Infos d'erreur (mode dev uniquement)
199        </h3>
200        <p className="font-mono text-sm mb-2">{error.message}</p>
201        {error.digest && (
202          <p className="font-mono text-xs text-gray-500 mb-2">Digest: {error.digest}</p>
203        )}
204        {error.stack && (
205          <pre className="mt-2 p-2 bg-gray-800 text-gray-200 rounded overflow-x-auto text-xs">
206            {error.stack}
207          </pre>
208        )}
209      </div>
210    );
211  }
212
213  return (
214    <div className="min-h-[400px] flex flex-col items-center justify-center p-6">
215      <div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8 text-center">
216        {renderErrorContent()}
217        {renderDevModeInfo()}
218      </div>
219    </div>
220  );
221}

Résumé

Les composants error.tsx en Next.js sont un puissant outil de gestion des erreurs qui :

  1. Isolent les erreurs à des segments précis de l'application
  2. Permettent d'afficher des messages conviviaux
  3. Permettent de tenter une récupération sans recharger la page
  4. Peuvent être personnalisés pour différents types d'erreurs
  5. Permettent l'intégration avec des systèmes de monitoring des erreurs

Grâce à eux, tu peux construire des applications plus résilientes qui gèrent élégamment les scénarios d'échec, améliorant l'expérience utilisateur globale et réduisant la frustration causée par les problèmes inattendus.

Code de cette leçon : App.tsx
1import React, { useState, Component, ReactNode } from 'react';
2
3// Simulation de error.tsx dans l'App Router de Next.js
4// Chaque segment peut avoir son propre error boundary
5
6// Error boundary simulé (comme error.tsx dans Next.js)
7class ErrorBoundary extends Component<
8  { children: ReactNode; fallback: (error: Error, reset: () => void) => ReactNode },
9  { error: Error | null }
10> {
11  state: { error: Error | null } = { error: null };
12  static getDerivedStateFromError(error: Error) { return { error }; }
13  reset = () => this.setState({ error: null });
14  render() {
15    if (this.state.error) return this.props.fallback(this.state.error, this.reset);
16    return this.props.children;
17  }
18}
19
20// Composant error.tsx - affiche en cas d'erreur
21function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
22  return (
23    <div style={{
24      background: '#3e1010', border: '1px solid #f44336', borderRadius: 8,
25      padding: 16, margin: '8px 0'
26    }}>
27      <h3 style={{ color: '#f44336', margin: '0 0 8px' }}>error.tsx</h3>
28      <p style={{ color: '#ffcdd2', fontSize: 13 }}>{error.message}</p>
29      <button onClick={reset} style={{
30        background: '#f44336', color: '#fff', border: 'none', borderRadius: 4,
31        padding: '6px 12px', cursor: 'pointer', fontFamily: 'monospace', marginTop: 8
32      }}>
33        Reessayer (reset)
34      </button>
35    </div>
36  );
37}
38
39// Composant qui peut lever une erreur
40function DangerousWidget({ label }: { label: string }) {
41  const [shouldError, setShouldError] = useState(false);
42  if (shouldError) throw new Error(`Erreur dans le composant "${label}" !`);
43  return (
44    <div style={{ background: '#1a2744', borderRadius: 8, padding: 16, border: '1px solid #64ffda' }}>
45      <h4 style={{ color: '#64ffda', margin: '0 0 8px' }}>{label}</h4>
46      <p style={{ color: '#aaa', fontSize: 12 }}>Ce composant fonctionne correctement.</p>
47      <button onClick={() => setShouldError(true)} style={{
48        background: '#ff5722', color: '#fff', border: 'none', borderRadius: 4,
49        padding: '4px 10px', cursor: 'pointer', fontFamily: 'monospace', fontSize: 11
50      }}>
51        Declencher une erreur
52      </button>
53    </div>
54  );
55}
56
57export default function ErrorTsxDemo() {
58  return (
59    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
60      <h1 style={{ color: '#64ffda' }}>error.tsx dans Next.js</h1>
61      <p style={{ color: '#888', fontSize: 13, marginBottom: 16 }}>
62        Chaque segment peut avoir son propre error.tsx. L'erreur est isolee - le reste de l'application fonctionne.
63      </p>
64
65      <div style={{ display: 'grid', gap: 12 }}>
66        <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
67          <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/dashboard/error.tsx</div>
68          <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
69            <DangerousWidget label="Dashboard Widget" />
70          </ErrorBoundary>
71        </div>
72
73        <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
74          <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/profile/error.tsx</div>
75          <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
76            <DangerousWidget label="Profile Widget" />
77          </ErrorBoundary>
78        </div>
79
80        <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
81          <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/settings/error.tsx (independant)</div>
82          <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
83            <DangerousWidget label="Settings Widget" />
84          </ErrorBoundary>
85        </div>
86      </div>
87    </div>
88  );
89}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Les labels ARIA dans les formulaires aident à :

Articles utiles