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

Error Boundaries - les gardiens de ton application

6 min de lecture
Dans cette leçon5

Dans la leçon précédente, tu as vu comment un seul module défaillant éteint toute la station, et qu'un try/catch autour du JSX ne sauve rien. Il nous faut un mécanisme qui agit là où React rend vraiment les enfants. Les Error Boundaries (limites d'erreur) sont des composants React spéciaux qui agissent comme les systèmes de sécurité d'une station spatiale. Quand un module tombe en panne, l'Error Boundary isole le problème et empêche la destruction de toute la station, exactement comme une cloison étanche ferme un compartiment dépressurisé.

Que sont les Error Boundaries ?

Une Error Boundary est un composant de classe React qui attrape les erreurs JavaScript n'importe où dans son arbre de composants enfants, les journalise et affiche une UI de repli (fallback) à la place de l'arbre qui a planté. Pourquoi un composant de classe ? La documentation react.dev est claire : actuellement, on ne peut pas écrire une Error Boundary sous forme de composant fonctionnel, car il n'existe aucun hook équivalent aux méthodes ci-dessous. Heureusement, tu écris un tel composant une seule fois et tu l'utilises dans toute la flotte.

Deux méthodes clés

Une Error Boundary est un composant de classe qui implémente l'une de ces méthodes, ou les deux :

  1. static getDerivedStateFromError(error) - met à jour l'état pour que le prochain rendu affiche une UI de repli
  2. componentDidCatch(error, errorInfo) - journalise les informations sur l'erreur (par ex. vers un service de monitoring)

L'ordre compte. Quand un enfant lève une erreur, React appelle d'abord getDerivedStateFromError pendant la phase de rendu, puis appelle render(), qui affiche déjà le fallback, et ce n'est qu'après la validation des changements à l'écran qu'il appelle componentDidCatch. C'est pourquoi la première méthode doit être pure (elle retourne seulement le nouvel état), tandis que les effets de bord, comme l'envoi d'un rapport, appartiennent à la seconde.

Implémentation de base

Voici une cloison complète et minimale. L'état hasError indique si le compartiment est dépressurisé, et error conserve l'erreur elle-même pour afficher son contenu :

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  // Appelée lorsqu'un composant enfant lève une erreur
10  // Retourne un objet qui met à jour l'état
11  static getDerivedStateFromError(error) {
12    return { hasError: true, error: error };
13  }
14
15  // Appelée après avoir attrapé une erreur
16  // Excellent endroit pour journaliser les erreurs
17  componentDidCatch(error, errorInfo) {
18    console.error('Error caught:', error);
19    console.error('Component stack:', errorInfo.componentStack);
20  }
21
22  render() {
23    if (this.state.hasError) {
24      return (
25        <div className="error-fallback">
26          <h2>Quelque chose s'est mal passé !</h2>
27          <p>{this.state.error?.message}</p>
28        </div>
29      );
30    }
31
32    return this.props.children;
33  }
34}

Tant que rien ne casse, render() retourne simplement this.props.children, donc la limite est invisible et ne change ni l'apparence ni le fonctionnement des enfants. Le champ errorInfo.componentStack est le chemin des composants qui mène à la panne, précieux pour en chercher la cause.

Utiliser les Error Boundaries

Une Error Boundary englobe les composants que tu veux protéger. Chaque module de la station reçoit sa propre cloison :

1function SpaceStation() {
2  return (
3    <div className="station">
4      <ErrorBoundary>
5        <Navigation />
6      </ErrorBoundary>
7
8      <ErrorBoundary>
9        <MainPanel />
10      </ErrorBoundary>
11
12      <ErrorBoundary>
13        <StatusBar />
14      </ErrorBoundary>
15    </div>
16  );
17}

Maintenant, si MainPanel lève une erreur, Navigation et StatusBar continueront de fonctionner ! La limite n'attrape que les erreurs de son propre sous-arbre, donc les modules voisins n'apprennent même pas qu'il y a eu une panne.

La granularité des Error Boundaries

Tu peux utiliser différents niveaux de granularité. Il est utile de les voir comme les couches de boucliers d'un vaisseau.

Niveau application (filet de sécurité général)

La limite la plus large entoure toute l'application et attrape tout ce que les limites plus étroites n'ont pas attrapé :

1function App() {
2  return (
3    <ErrorBoundary fallback={<h1>Panne de toute la station !</h1>}>
4      <SpaceStation />
5    </ErrorBoundary>
6  );
7}

Cette limite ne protégera pas les voisins, car elle remplace tout par le fallback, mais elle transforme l'écran blanc en un message lisible. Nous gérerons la prop fallback dans notre classe dans un instant.

Niveau section (isolation des modules)

Des limites plus étroites entourent des sections plus grandes, chacune avec son propre message :

1function SpaceStation() {
2  return (
3    <div>
4      <ErrorBoundary fallback={<p>Navigation hors ligne</p>}>
5        <Navigation />
6      </ErrorBoundary>
7      <ErrorBoundary fallback={<p>Panneau de contrôle indisponible</p>}>
8        <ControlPanel />
9      </ErrorBoundary>
10    </div>
11  );
12}

Une panne du panneau de contrôle ne coupe que son compartiment, et la navigation continue de guider le vaisseau.

Niveau composant (isolation précise)

La limite la plus fine protège des widgets individuels, par exemple chaque tuile du tableau de bord :

1function Dashboard() {
2  return (
3    <div className="dashboard">
4      {widgets.map(widget => (
5        <ErrorBoundary key={widget.id} fallback={<WidgetError />}>
6          <Widget data={widget} />
7        </ErrorBoundary>
8      ))}
9    </div>
10  );
11}

Remarque que la key est placée sur ErrorBoundary, car c'est maintenant lui l'élément direct de la liste. Mon conseil : commence par une limite au niveau de l'application et ajoute des limites plus étroites là où une panne est probable, par exemple pour les données venant d'API externes.

Ce que les Error Boundaries n'attrapent PAS

Il est important de savoir que les Error Boundaries n'attrapent pas les erreurs dans :

  1. Les gestionnaires d'événements - utilise try/catch
  2. Le code asynchrone - setTimeout, fetch, Promise
  3. Le rendu côté serveur (SSR)
  4. L'Error Boundary elle-même (et non ses enfants)

Une exception au point 2 est apparue dans React 19 : les erreurs levées à l'intérieur de la fonction passée à startTransition du hook useTransition arrivent à la limite la plus proche. Dans un gestionnaire ordinaire, tu as toujours besoin de try/catch :

1// Gestionnaire d'événement - nécessite try/catch
2function Button() {
3  const handleClick = () => {
4    try {
5      riskyOperation();
6    } catch (err) {
7      // Gère l'erreur ici
8    }
9  };
10  return <button onClick={handleClick}>Clique</button>;
11}

La limite au-dessus de ce bouton ne réagira pas à une erreur venant de handleClick, car au moment du clic React ne rend rien. La gestion revient au gestionnaire.

Fallback personnalisé avec une prop

La bonne pratique consiste à créer une Error Boundary réutilisable avec un fallback configurable. Cette version vérifie si le parent a transmis la prop fallback, et n'affiche un message par défaut que si elle est absente :

1class ErrorBoundary extends React.Component {
2  constructor(props) {
3    super(props);
4    this.state = { hasError: false, error: null };
5  }
6
7  static getDerivedStateFromError(error) {
8    return { hasError: true, error };
9  }
10
11  componentDidCatch(error, info) {
12    console.error('Error boundary caught:', error, info);
13  }
14
15  render() {
16    if (this.state.hasError) {
17      // Utilise la prop fallback si elle est fournie
18      if (this.props.fallback) {
19        return this.props.fallback;
20      }
21      return <h2>Une erreur est survenue</h2>;
22    }
23    return this.props.children;
24  }
25}
26
27// Utilisation avec un fallback personnalisé
28<ErrorBoundary fallback={<p>Module endommagé, mais la station tourne !</p>}>
29  <RiskyModule />
30</ErrorBoundary>

La logique d'interception n'a pas changé, seul ce que voit l'équipage a changé. Dans la prochaine leçon, tu découvriras la bibliothèque react-error-boundary, qui ajoute à une telle classe la réinitialisation, le nettoyage automatique et un hook pour les erreurs asynchrones.

Souviens-toi : une Error Boundary est une cloison de secours qui ferme un seul compartiment, pour que le reste du vaisseau puisse continuer son vol.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Error Boundary - composant de classe
5class ErrorBoundary extends React.Component {
6  constructor(props) {
7    super(props);
8    this.state = { hasError: false, error: null };
9  }
10
11  static getDerivedStateFromError(error) {
12    return { hasError: true, error };
13  }
14
15  componentDidCatch(error, errorInfo) {
16    console.error('ErrorBoundary a attrapé :', error);
17    console.error('Pile des composants :', errorInfo.componentStack);
18  }
19
20  render() {
21    if (this.state.hasError) {
22      return (
23        <div className="error-fallback">
24          <h3>Module endommagé</h3>
25          <p>{this.state.error?.message}</p>
26          <button
27            onClick={() => this.setState({ hasError: false })}
28            className="retry-btn"
29          >
30            Redémarrer le module
31          </button>
32        </div>
33      );
34    }
35    return this.props.children;
36  }
37}
38
39// Composant qui peut lever une erreur
40function UnstableModule({ name, failRate = 0.5 }) {
41  if (Math.random() < failRate) {
42    throw new Error(name + ' - panne du système !');
43  }
44  return (
45    <div className="module-ok">
46      <span className="status-dot green" />
47      {name} : Opérationnel
48    </div>
49  );
50}
51
52// Composant stable
53function StableModule({ name }) {
54  return (
55    <div className="module-ok">
56      <span className="status-dot green" />
57      {name} : Opérationnel
58    </div>
59  );
60}
61
62export default function App() {
63  const [key, setKey] = useState(0);
64
65  return (
66    <div className="app">
67      <h1>Error Boundaries</h1>
68      <p className="subtitle">
69        Chaque module est protégé par sa propre Error Boundary
70      </p>
71      <button
72        onClick={() => setKey(k => k + 1)}
73        className="reset-btn"
74      >
75        Réinitialiser la station (key : {key})
76      </button>
77
78      <div className="station" key={key}>
79        <ErrorBoundary>
80          <UnstableModule name="Moteur principal" failRate={0.4} />
81        </ErrorBoundary>
82
83        <ErrorBoundary>
84          <UnstableModule name="Communication" failRate={0.4} />
85        </ErrorBoundary>
86
87        <ErrorBoundary>
88          <StableModule name="Systèmes de survie" />
89        </ErrorBoundary>
90
91        <ErrorBoundary>
92          <UnstableModule name="Capteurs" failRate={0.4} />
93        </ErrorBoundary>
94      </div>
95    </div>
96  );
97}

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. Que fait la méthode static getDerivedStateFromError(error) dans une Error Boundary ?

  2. 2. Quelles erreurs une Error Boundary n'attrape-t-elle PAS ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Clique dans l'ordre les étapes du fonctionnement d'une Error Boundary après une erreur :

  • Éditeur de code

    Implémente une Error Boundary

  • Clique dans l'ordre

    Clique dans l'ordre les éléments nécessaires pour créer une Error Boundary :

  • Éditeur de code

    Implémente une Error Boundary personnalisée

  • Tri vertical

    Ordonne les niveaux de granularité des Error Boundaries du plus large au plus étroit :

Articles utiles