Cours JavaScript et React · Module 16: Gestion des erreurs et Suspense

Modèles avancés de gestion des erreurs

6 min de lecture
Dans cette leçon5

Tes limites d'erreur attrapent les pannes et affichent un fallback, mais réfléchis : comment sauras-tu qu'à trois heures du matin, mille personnes ont vu le message "Quelque chose s'est mal passé" ? L'utilisateur ne t'enverra pas sa console, et tu n'es pas assis devant chaque navigateur. Dans cette leçon, nous allons explorer des techniques avancées de gestion des erreurs que tu peux appliquer dans des applications React complexes. Ces modèles sont comme les systèmes de sécurité avancés d'une station spatiale - multi-couches, intelligents et flexibles.

Monitoring et journalisation des erreurs

En production, tu dois être informé des erreurs immédiatement. Intègre l'Error Boundary à un système de monitoring. La classe ci-dessous fait trois choses : elle génère un identifiant de panne, envoie un rapport au service (la fonction logErrorToService est ton client, par exemple Sentry) et affiche à l'utilisateur un code qu'il peut communiquer au support :

1class MonitoredErrorBoundary extends React.Component {
2  constructor(props) {
3    super(props);
4    this.state = { hasError: false, error: null, errorId: null };
5  }
6
7  static getDerivedStateFromError(error) {
8    return { hasError: true, error };
9  }
10
11  componentDidCatch(error, errorInfo) {
12    // Génère un identifiant d'erreur unique
13    const errorId = Date.now().toString(36);
14    this.setState({ errorId });
15
16    // Envoi au service de monitoring
17    logErrorToService({
18      errorId,
19      message: error.message,
20      stack: error.stack,
21      componentStack: errorInfo.componentStack,
22      timestamp: new Date().toISOString(),
23      url: window.location.href,
24      userAgent: navigator.userAgent
25    });
26  }
27
28  render() {
29    if (this.state.hasError) {
30      return (
31        <div className="error-report">
32          <h3>Une erreur est survenue</h3>
33          <p>Code d'erreur : {this.state.errorId}</p>
34          <p>Notre équipe a été prévenue.</p>
35          <button onClick={() => this.setState({ hasError: false })}>
36            Réessayer
37          </button>
38        </div>
39      );
40    }
41    return this.props.children;
42  }
43}

Nous envoyons le rapport dans componentDidCatch, et non dans getDerivedStateFromError, car cette dernière s'exécute pendant la phase de rendu et doit être pure. Date.now().toString(36) n'est qu'un simple identifiant basé sur le temps, et deux pannes survenues dans la même milliseconde recevraient le même code. En production, utilise crypto.randomUUID() ou l'identifiant renvoyé par le service de monitoring.

Gestion des erreurs sensible au contexte

Différentes parties de l'application peuvent nécessiter une gestion des erreurs différente. Une panne du système de survie est une alarme, une panne du widget météo est une ligne dans le journal de bord. Le contexte React (createContext et useContext) permet de transmettre cette politique à tout un sous-arbre sans passer de props :

1import { createContext, useContext } from 'react';
2
3const ErrorContext = createContext({
4  onError: (error) => console.error(error),
5  severity: 'normal'
6});
7
8function useErrorHandler() {
9  const { onError, severity } = useContext(ErrorContext);
10
11  return (error) => {
12    if (severity === 'critical') {
13      // Pour les sections critiques - envoie une alerte
14      sendAlert(error);
15    }
16    onError(error);
17  };
18}
19
20// Utilisation
21function CriticalSection({ children }) {
22  return (
23    <ErrorContext.Provider value={{
24      onError: (err) => sendToMonitoring(err),
25      severity: 'critical'
26    }}>
27      <ErrorBoundary>
28        {children}
29      </ErrorBoundary>
30    </ErrorContext.Provider>
31  );
32}

Chaque composant à l'intérieur de CriticalSection appellera useErrorHandler() et recevra une fonction qui sait d'elle-même s'il faut déclencher l'alarme. Dans React 19, tu peux aussi écrire plus court <ErrorContext value={...}>, l'écriture avec .Provider fonctionne toujours.

Stratégies de récupération après erreur

Afficher l'erreur n'est qu'un début. Voici deux stratégies de récupération.

Stratégie 1 : rafraîchissement automatique du composant

Cette stratégie utilise l'ErrorBoundary de la bibliothèque react-error-boundary et une astuce avec la prop key. Quand la key change, React détruit l'ancien composant et en crée un nouveau, avec un état vierge :

1function AutoRecoveryBoundary({ children, maxRetries = 3 }) {
2  const [retryCount, setRetryCount] = useState(0);
3  const [key, setKey] = useState(0);
4
5  const handleReset = () => {
6    if (retryCount < maxRetries) {
7      setRetryCount(prev => prev + 1);
8      setKey(prev => prev + 1); // Force un remontage
9    }
10  };
11
12  return (
13    <ErrorBoundary
14      key={key}
15      FallbackComponent={({ error }) => (
16        <div>
17          <p>Erreur : {error.message}</p>
18          {retryCount < maxRetries ? (
19            <button onClick={handleReset}>
20              Réessayer ({maxRetries - retryCount} tentatives restantes)
21            </button>
22          ) : (
23            <p>Impossible de corriger l'erreur. Contacte le support.</p>
24          )}
25        </div>
26      )}
27    >
28      {children}
29    </ErrorBoundary>
30  );
31}

Le compteur retryCount vit en dehors de la limite, donc le remontage ne le remet pas à zéro, et c'est pourquoi la limite de tentatives fonctionne. Tout ce qui se trouve à l'intérieur de la limite repart de zéro.

Stratégie 2 : repli vers une version plus simple

Au lieu d'un message, tu peux afficher une version plus pauvre, mais fonctionnelle, de la même information. Le graphique interactif dépend d'une bibliothèque lourde, un simple tableau non :

1function ChartWithFallback({ data }) {
2  return (
3    <ErrorBoundary
4      FallbackComponent={() => (
5        // Au lieu d'un graphique interactif, affiche un simple tableau
6        <table>
7          <thead><tr><th>Nom</th><th>Valeur</th></tr></thead>
8          <tbody>
9            {data.map(item => (
10              <tr key={item.id}>
11                <td>{item.name}</td>
12                <td>{item.value}</td>
13              </tr>
14            ))}
15          </tbody>
16        </table>
17      )}
18    >
19      <InteractiveChart data={data} />
20    </ErrorBoundary>
21  );
22}

Les données sont les mêmes, seule leur présentation change. L'équipage voit toujours les relevés, même sans animation.

Gestion globale des erreurs

Attrape les erreurs non gérées au niveau global. L'événement error sur window attrape les exceptions que personne n'a attrapées, par exemple dans les gestionnaires et les timers, et unhandledrejection attrape les promesses rejetées sans .catch() :

1// Dans le composant principal de l'application (par ex. App)
2useEffect(() => {
3  const handleUnhandledError = (event) => {
4    console.error('Unhandled error:', event.error);
5    // Envoi au monitoring
6  };
7
8  const handleUnhandledRejection = (event) => {
9    console.error('Unhandled promise rejection:', event.reason);
10    // Envoi au monitoring
11  };
12
13  window.addEventListener('error', handleUnhandledError);
14  window.addEventListener('unhandledrejection', handleUnhandledRejection);
15
16  return () => {
17    window.removeEventListener('error', handleUnhandledError);
18    window.removeEventListener('unhandledrejection', handleUnhandledRejection);
19  };
20}, []);

La fonction de nettoyage retire les deux écouteurs au démontage, donc ils ne se dupliquent pas. Garde en tête que, selon react.dev, en mode développement les erreurs attrapées par componentDidCatch arrivent aussi jusqu'à window, mais plus en production, donc en dev tu verras certaines erreurs deux fois.

React 19 y a ajouté une deuxième couche : les options onCaughtError et onUncaughtError de la fonction createRoot. La première reçoit les erreurs attrapées par n'importe quelle limite, la seconde celles qu'aucune n'a attrapées :

1import { createRoot } from 'react-dom/client';
2
3const root = createRoot(document.getElementById('root'), {
4  onCaughtError: (error, errorInfo) => {
5    // Erreur attrapée par une Error Boundary
6    reportToMonitoring('caught', error, errorInfo.componentStack);
7  },
8  onUncaughtError: (error, errorInfo) => {
9    // Erreur qu'aucune limite n'a attrapée
10    reportToMonitoring('uncaught', error, errorInfo.componentStack);
11  }
12});
13root.render(<App />);

Les deux callbacks reçoivent componentStack, tu as donc un seul endroit central pour signaler toutes les erreurs de rendu. Par défaut, React se contente de les journaliser dans la console.

Résumé des modèles

Rassemblons toutes les couches de boucliers dans un seul tableau :

ModèleQuand l'utiliser
Error BoundaryErreurs de rendu des composants
try/catchGestionnaires d'événements et code synchrone
useErrorBoundaryErreurs asynchrones dans une Error Boundary
RetryConnexions réseau instables
Fallback contentDonnées optionnelles
Gestionnaire globalErreurs inattendues

Mon conseil : dans un nouveau projet React 19, commence par onCaughtError et onUncaughtError dans createRoot, car c'est le moyen le moins coûteux pour qu'aucune erreur de rendu ne passe inaperçue. Dans le projet principal de cette localisation, tu combineras tous ces modèles en un seul tableau de bord de mission résilient.

Souviens-toi : une bonne station ne se contente pas de survivre à une panne, elle la signale aussi immédiatement au centre de contrôle de mission.

Code de cette leçon : App.jsx
1import React, { useState, useEffect, createContext, useContext } from 'react';
2import './styles.css';
3
4// Error Logging Context
5const ErrorLogContext = createContext({ logs: [], addLog: () => {} });
6
7function useErrorLog() {
8  return useContext(ErrorLogContext);
9}
10
11// Monitored Error Boundary
12class MonitoredBoundary extends React.Component {
13  constructor(props) {
14    super(props);
15    this.state = { hasError: false, error: null, retries: 0 };
16  }
17  static getDerivedStateFromError(error) {
18    return { hasError: true, error };
19  }
20  componentDidCatch(error, info) {
21    const log = {
22      id: Date.now(),
23      section: this.props.section || 'unknown',
24      message: error.message,
25      time: new Date().toLocaleTimeString(),
26    };
27    if (this.props.onLogError) {
28      this.props.onLogError(log);
29    }
30  }
31  handleRetry = () => {
32    if (this.state.retries < (this.props.maxRetries || 3)) {
33      this.setState(s => ({
34        hasError: false,
35        error: null,
36        retries: s.retries + 1,
37      }));
38    }
39  };
40  render() {
41    if (this.state.hasError) {
42      const remaining = (this.props.maxRetries || 3) - this.state.retries;
43      return (
44        <div className="monitored-error">
45          <h4>{this.props.section} - Panne</h4>
46          <p>{this.state.error.message}</p>
47          {remaining > 0 ? (
48            <button onClick={this.handleRetry} className="retry-btn">
49              Réessayer ({remaining} tentatives)
50            </button>
51          ) : (
52            <p className="no-retry">Plus de tentatives - signale l'erreur</p>
53          )}
54        </div>
55      );
56    }
57    return this.props.children;
58  }
59}
60
61function UnstableWidget({ name, fail = 0.5 }) {
62  if (Math.random() < fail) {
63    throw new Error(name + ' : panne du système');
64  }
65  return (
66    <div className="widget-ok">
67      <span className="dot" /> {name} : En ligne
68    </div>
69  );
70}
71
72// Panneau du journal des erreurs
73function ErrorLogPanel({ logs }) {
74  if (logs.length === 0) return (
75    <div className="log-panel">
76      <h3>Journal des erreurs</h3>
77      <p className="empty">Aucune erreur enregistrée</p>
78    </div>
79  );
80  return (
81    <div className="log-panel">
82      <h3>Journal des erreurs ({logs.length})</h3>
83      {logs.map(log => (
84        <div key={log.id} className="log-entry">
85          <span className="log-time">{log.time}</span>
86          <span className="log-section">[{log.section}]</span>
87          <span className="log-msg">{log.message}</span>
88        </div>
89      ))}
90    </div>
91  );
92}
93
94export default function App() {
95  const [key, setKey] = useState(0);
96  const [logs, setLogs] = useState([]);
97
98  const addLog = (log) => {
99    setLogs(prev => [log, ...prev].slice(0, 10));
100  };
101
102  return (
103    <div className="app">
104      <h1>Error Monitoring</h1>
105      <button onClick={() => setKey(k => k + 1)} className="reset-btn">
106        Redémarrer les systèmes
107      </button>
108
109      <div className="grid" key={key}>
110        <MonitoredBoundary section="Moteurs" maxRetries={3} onLogError={addLog}>
111          <UnstableWidget name="Moteurs" fail={0.5} />
112        </MonitoredBoundary>
113
114        <MonitoredBoundary section="Navigation" maxRetries={3} onLogError={addLog}>
115          <UnstableWidget name="Navigation" fail={0.5} />
116        </MonitoredBoundary>
117
118        <MonitoredBoundary section="Liaison" maxRetries={3} onLogError={addLog}>
119          <UnstableWidget name="Liaison" fail={0.5} />
120        </MonitoredBoundary>
121
122        <MonitoredBoundary section="Capteurs" maxRetries={3} onLogError={addLog}>
123          <UnstableWidget name="Capteurs" fail={0.5} />
124        </MonitoredBoundary>
125      </div>
126
127      <ErrorLogPanel logs={logs} />
128    </div>
129  );
130}

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. Quelle méthode d'Error Boundary est la meilleure pour journaliser les erreurs vers un service externe ?

  2. 2. En quoi consiste la stratégie de « repli vers une version plus simple d'un composant » ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les stratégies de récupération après erreur du plus simple au plus complexe :

  • Éditeur de code

    Implémente RetryErrorBoundary avec maxRetries

  • Tri horizontal

    Ordonne la syntaxe pour ajouter un gestionnaire d'erreurs global :

  • Tri vertical

    Ordonne les étapes pour construire une application résiliente :

  • Éditeur de code

    Implémente des onglets avec lazy loading

  • Tri vertical

    Ordonne les éléments de la page par priorité de chargement (de la plus haute) :

  • Tri horizontal

    Ordonne la syntaxe d'ErrorBoundary avec resetKeys :

  • Éditeur de code

    Construis un mini tableau de bord résilient

  • Tri vertical

    Ordonne les couches de résilience du plus externe au plus interne :

Articles utiles