Cours Next.js · Module 6: Formulaires et Server Actions
Les composants error.tsx pour la gestion des erreurs
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 :
- Next.js capture automatiquement l'erreur
- Rend le composant error.tsx à la place du composant qui a produit l'erreur
- 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 :
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.
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
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.tsx2. 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
Granularité appropriée - utilise error.tsx à des niveaux qui offrent une isolation d'erreur pertinente, sans créer trop de composants.
Messages utiles - fournis des messages conviviaux et des instructions claires pour les étapes suivantes.
Options de récupération - propose des moyens de se remettre de la situation, comme un bouton « Réessayer » (via la fonction
reset).Suivi et monitoring - journalise toujours les erreurs dans les systèmes de monitoring pour pouvoir les suivre et les résoudre.
Tests - teste divers scénarios d'erreur pour t'assurer que tes frontières d'erreur réagissent correctement.
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.tsxStructure 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.tsxExemple 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 :
- Isolent les erreurs à des segments précis de l'application
- Permettent d'afficher des messages conviviaux
- Permettent de tenter une récupération sans recharger la page
- Peuvent être personnalisés pour différents types d'erreurs
- 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. Les labels ARIA dans les formulaires aident à :