Cours JavaScript et React · Module 11: Récupération de données et API
Gestion avancée des erreurs
Dans cette leçon4
Dans l'espace, les choses ne se passent souvent pas comme prévu : la communication peut être interrompue, les serveurs peuvent être surchargés, les données peuvent être corrompues. Jusqu'ici, chaque erreur se terminait par un seul message du genre « Quelque chose s'est mal passé ». L'utilisateur ne sait alors pas s'il doit se connecter, attendre ou vérifier son Wi-Fi. Une bonne gestion des erreurs fait la différence entre une mission réussie et une catastrophe. Dans cette leçon, nous allons construire quatre couches de protection : des messages clairs, un Error Boundary, des notifications toast et des réessais avec un délai croissant.
Try/Catch/Finally - le pattern complet
La première couche transforme les codes de statut HTTP en phrases compréhensibles par un humain. L'instruction switch choisit le message selon le code, et catch distingue les erreurs du serveur des erreurs réseau. Quand il n'y a aucune connexion, fetch() rejette sa Promise avec une erreur de type TypeError :
1async function fetchWithFullErrorHandling(url) {
2 try {
3 // Tentative de récupération des données
4 const response = await fetch(url);
5
6 if (!response.ok) {
7 // Messages différents selon le code d'erreur
8 switch (response.status) {
9 case 400:
10 throw new Error('Requête invalide');
11 case 401:
12 throw new Error('Non autorisé - connecte-toi');
13 case 403:
14 throw new Error('Accès refusé à cette ressource');
15 case 404:
16 throw new Error('Ressource introuvable');
17 case 429:
18 throw new Error('Trop de requêtes - réessaie plus tard');
19 case 500:
20 throw new Error('Erreur serveur - réessaie plus tard');
21 default:
22 throw new Error(`Erreur inconnue : ${response.status}`);
23 }
24 }
25
26 return await response.json();
27 } catch (err) {
28 // Gestion des erreurs réseau (pas d'internet, timeout)
29 if (err instanceof TypeError) {
30 throw new Error(
31 'Erreur réseau - vérifie ta connexion internet'
32 );
33 }
34 throw err; // Relancer les autres erreurs
35 }
36}Nos propres erreurs issues du switch sont de simples Error, pas des TypeError, donc le catch les laisse passer sans les modifier (throw err). La fonction retourne toujours des données ou lève une erreur, seule la qualité du message a changé.
Le composant ErrorBoundary
La deuxième couche protège contre une erreur qui ferait exploser toute l'interface. Quand un composant lève une exception pendant le rendu, React démonte tout l'arbre et l'utilisateur voit une page blanche. Un Error Boundary est un composant qui attrape les erreurs de rendu de ses enfants et affiche une vue de secours. Même en React 19, on ne peut l'écrire que sous forme de composant de classe, car il a besoin des méthodes de cycle de vie static getDerivedStateFromError (définit l'état d'erreur) et componentDidCatch (par exemple, journalise l'erreur) :
1import React from 'react';
2
3class ErrorBoundary extends React.Component {
4 constructor(props) {
5 super(props);
6 this.state = { hasError: false, error: null };
7 }
8
9 static getDerivedStateFromError(error) {
10 return { hasError: true, error };
11 }
12
13 componentDidCatch(error, errorInfo) {
14 console.error('ErrorBoundary a attrapé une erreur :', error);
15 console.error('Stack :', errorInfo.componentStack);
16 }
17
18 render() {
19 if (this.state.hasError) {
20 return (
21 <div className="error-boundary">
22 <h2>Quelque chose s'est mal passé</h2>
23 <p>{this.state.error?.message}</p>
24 <button onClick={() => this.setState({
25 hasError: false, error: null
26 })}>
27 Réessayer
28 </button>
29 </div>
30 );
31 }
32
33 return this.props.children;
34 }
35}
36
37// Utilisation :
38function App() {
39 return (
40 <ErrorBoundary>
41 <SpaceDashboard />
42 </ErrorBoundary>
43 );
44}Quand SpaceDashboard explose, le reste de l'application survit, et le bouton réinitialise l'état et tente de rendre à nouveau les enfants. Une limite très importante tirée de la documentation de React : un Error Boundary n'attrape pas les erreurs dans les gestionnaires d'événements ni dans le code asynchrone, comme un fetch() rejeté dans useEffect. Celles-là, tu les gères avec try/catch et un état error. En pratique, je te recommande le paquet tout prêt react-error-boundary pour ne pas écrire cette classe toi-même.
Notifications Toast
La troisième couche, ce sont des messages qui ne masquent pas le contenu. Un toast est une petite bulle dans un coin de l'écran qui disparaît après quelques secondes. Pour que n'importe quel composant puisse en déclencher un, on expose une fonction addToast via un Context :
1import React, { useState, createContext, useContext } from 'react';
2
3const ToastContext = createContext();
4
5function ToastProvider({ children }) {
6 const [toasts, setToasts] = useState([]);
7
8 const addToast = (message, type = 'error') => {
9 const id = Date.now();
10 setToasts(prev => [...prev, { id, message, type }]);
11 // Suppression automatique après 5 secondes
12 setTimeout(() => {
13 setToasts(prev => prev.filter(t => t.id !== id));
14 }, 5000);
15 };
16
17 return (
18 <ToastContext.Provider value={{ addToast }}>
19 {children}
20 <div className="toast-container">
21 {toasts.map(toast => (
22 <div key={toast.id} className={`toast toast-${toast.type}`}>
23 {toast.message}
24 </div>
25 ))}
26 </div>
27 </ToastContext.Provider>
28 );
29}
30
31function useToast() {
32 return useContext(ToastContext);
33}Le provider garde la liste des toasts dans son état, et setTimeout supprime chacun après 5 secondes. Le hook useToast n'est qu'un raccourci vers useContext. Désormais, n'importe quel composant à l'intérieur du provider peut signaler un succès ou une erreur :
1// Utilisation dans un composant :
2function DataComponent() {
3 const { addToast } = useToast();
4
5 const fetchData = async () => {
6 try {
7 const response = await fetch('/api/data');
8 if (!response.ok) throw new Error('Erreur serveur');
9 const data = await response.json();
10 addToast('Données récupérées avec succès !', 'success');
11 } catch (err) {
12 addToast(err.message, 'error');
13 }
14 };
15
16 return <button onClick={fetchData}>Récupérer les données</button>;
17}Le composant n'affiche lui-même aucun message, il le signale simplement plus haut. Un détail : Date.now() comme id peut se répéter pour deux toasts créés dans la même milliseconde, donc en production utilise crypto.randomUUID() ou un compteur.
Retry avec Exponential Backoff
La quatrième couche corrige les erreurs passagères. Quand le serveur est surchargé (429) ou en panne (5xx), réessayer immédiatement ne fait qu'aggraver les choses. L'exponential backoff consiste à attendre de plus en plus longtemps : 1 s, 2 s, 4 s. On calcule le délai avec Math.pow(2, attempt) * 1000 :
1async function fetchWithRetry(url, maxRetries = 3) {
2 for (let attempt = 0; attempt <= maxRetries; attempt++) {
3 try {
4 const response = await fetch(url);
5
6 if (response.status === 429 || response.status >= 500) {
7 // Serveur surchargé ou erreur serveur
8 if (attempt < maxRetries) {
9 // Exponential backoff : 1s, 2s, 4s
10 const delay = Math.pow(2, attempt) * 1000;
11 console.log(
12 `Tentative ${attempt + 1} échouée. \
13Nouvel essai dans ${delay}ms...`
14 );
15 await new Promise(resolve =>
16 setTimeout(resolve, delay)
17 );
18 continue;
19 }
20 }
21
22 if (!response.ok) {
23 throw new Error(`HTTP Error: ${response.status}`);
24 }
25
26 return await response.json();
27 } catch (err) {
28 if (attempt === maxRetries) {
29 throw new Error(
30 `Échec après ${maxRetries + 1} tentatives : ${err.message}`
31 );
32 }
33
34 const delay = Math.pow(2, attempt) * 1000;
35 await new Promise(resolve => setTimeout(resolve, delay));
36 }
37 }
38}Avec maxRetries = 3, la fonction essaie quatre fois au total, et après le dernier échec elle lève une erreur indiquant le nombre de tentatives. Remarque que le catch réessaie toutes les erreurs, y compris une 404, pour laquelle réessayer ne sert à rien. Dans un vrai projet, ne réessaie que les erreurs réseau, 429 et 5xx, et TanStack Query, vu dans la leçon précédente, le fera pour toi (trois réessais avec backoff par défaut).
Retiens : un bon message d'erreur dit à l'équipage ce qui s'est passé et ce qu'il peut faire ensuite, alors ne le laisse jamais dans le noir.
Code de cette leçon : App.jsx
1import React, { useState, createContext, useContext } from 'react';
2
3// Toast Context
4const ToastContext = createContext();
5
6function ToastProvider({ children }) {
7 const [toasts, setToasts] = useState([]);
8
9 const addToast = (message, type = 'error') => {
10 const id = Date.now();
11 setToasts(prev => [...prev, { id, message, type }]);
12 setTimeout(() => {
13 setToasts(prev => prev.filter(t => t.id !== id));
14 }, 4000);
15 };
16
17 const removeToast = (id) => {
18 setToasts(prev => prev.filter(t => t.id !== id));
19 };
20
21 return (
22 <ToastContext.Provider value={{ addToast }}>
23 {children}
24 <div className="toast-container">
25 {toasts.map(toast => (
26 <div key={toast.id} className={"toast toast-" + toast.type}>
27 <span>{toast.message}</span>
28 <button className="toast-close" onClick={() => removeToast(toast.id)}>x</button>
29 </div>
30 ))}
31 </div>
32 </ToastContext.Provider>
33 );
34}
35
36function useToast() {
37 return useContext(ToastContext);
38}
39
40// Simulation de différentes réponses du serveur
41function simulateResponse(scenario) {
42 return new Promise((resolve, reject) => {
43 setTimeout(() => {
44 switch (scenario) {
45 case 'success':
46 resolve({ ok: true, status: 200, json: () => Promise.resolve({ data: 'Données récupérées !' }) });
47 break;
48 case '404':
49 resolve({ ok: false, status: 404, statusText: 'Not Found' });
50 break;
51 case '500':
52 resolve({ ok: false, status: 500, statusText: 'Internal Server Error' });
53 break;
54 case 'network':
55 reject(new TypeError('Failed to fetch'));
56 break;
57 default:
58 resolve({ ok: true, status: 200 });
59 }
60 }, 1000);
61 });
62}
63
64function ErrorDemo() {
65 const { addToast } = useToast();
66 const [loading, setLoading] = useState(false);
67
68 const testScenario = async (scenario) => {
69 setLoading(true);
70 try {
71 const response = await simulateResponse(scenario);
72
73 if (!response.ok) {
74 switch (response.status) {
75 case 400: throw new Error('Requête invalide');
76 case 401: throw new Error('Non autorisé');
77 case 404: throw new Error('Ressource introuvable');
78 case 429: throw new Error('Trop de requêtes');
79 case 500: throw new Error('Erreur serveur');
80 default: throw new Error(`Erreur HTTP : ${response.status}`);
81 }
82 }
83
84 const data = await response.json();
85 addToast(data.data, 'success');
86 } catch (err) {
87 if (err instanceof TypeError) {
88 addToast('Erreur réseau - vérifie ta connexion', 'error');
89 } else {
90 addToast(err.message, 'error');
91 }
92 } finally {
93 setLoading(false);
94 }
95 };
96
97 return (
98 <div className="demo">
99 <h1>Gestion des erreurs - Démo</h1>
100 <p>Clique sur un bouton pour simuler différentes réponses du serveur :</p>
101
102 <div className="button-grid">
103 <button className="btn success" onClick={() => testScenario('success')} disabled={loading}>
104 200 OK (Succès)
105 </button>
106 <button className="btn warning" onClick={() => testScenario('404')} disabled={loading}>
107 404 Not Found
108 </button>
109 <button className="btn danger" onClick={() => testScenario('500')} disabled={loading}>
110 500 Server Error
111 </button>
112 <button className="btn danger" onClick={() => testScenario('network')} disabled={loading}>
113 Network Error
114 </button>
115 </div>
116
117 {loading && <p className="status">Envoi de la requête...</p>}
118 </div>
119 );
120}
121
122function App() {
123 return (
124 <ToastProvider>
125 <ErrorDemo />
126 </ToastProvider>
127 );
128}
129
130export default App;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. Qu'est-ce qu'une Error Boundary en React et quand s'active-t-elle ?
Tâches pratiques dans le jeu
- Tri vertical
Ordonne les délais croissants de la stratégie exponential backoff (du plus court au plus long) :
- Éditeur de code
Système de Toast
- Clique dans l'ordre
Clique sur les éléments dans l'ordre du bloc try/catch/finally :
- Clique dans l'ordre
Ordonne la syntaxe de gestion des erreurs avec async/await :
- Tri horizontal
Ordonne la syntaxe d'une condition qui vérifie le code de statut de la réponse HTTP :