Advanced (Mis à jour: 02/06/2026)

Error Boundaries React avec Claude Code : guide de mise en oeuvre sure

Implementer des Error Boundaries React avec Claude Code : portee, placement, reset, logs surs, tests et prompts.

Error Boundaries React avec Claude Code : guide de mise en oeuvre sure

La panne frontend la plus dangereuse n’est pas un graphique qui se casse. C’est une petite exception de rendu qui transforme toute l’application React en page blanche. Si vous demandez simplement a Claude Code “ajoute une gestion d’erreurs”, il peut ajouter quelques try/catch sans couvrir les erreurs qui se produisent pendant le rendu React.

Ce guide montre comment implementer des React Error Boundaries avec Claude Code de maniere sure. Un Error Boundary est une frontiere dans l’arbre de composants : quand un descendant lance une erreur inattendue pendant le rendu, il affiche une UI fallback au lieu de laisser toute la vue s’effondrer. Il ne corrige pas la cause, mais il limite l’impact, donne une action claire a l’utilisateur et produit un log exploitable.

Masa l’a teste sur un tableau de bord admin. Le premier prompt a entoure toute l’application avec un seul boundary. Cela a evite la page blanche, mais un graphique casse masquait aussi les parametres et la facturation, et les logs contenaient des URLs avec des emails dans les query strings. Quand le prompt a precise le placement, les regles de reset, la redaction de PII et les tests, le diff produit par Claude Code est devenu beaucoup plus facile a relire.

Partir Des Faits De La Documentation React

Avant de demander le code, fixez les faits. La reference officielle React de Component explique le partage des roles : static getDerivedStateFromError sert a passer en etat fallback, et componentDidCatch sert aux effets secondaires comme le logging. La documentation officielle du lint error-boundaries rappelle aussi qu’un try/catch autour du JSX n’est pas l’outil adapte pour les erreurs de rendu.

La contrainte pratique est simple : un Error Boundary ne capture pas tout. Il capture les erreurs inattendues lancees par des composants descendants pendant le rendu, les lifecycles ou le code execute dans le flux de rendu. Il ne capture pas les gestionnaires de clic, les timers, les promesses ordinaires, le rendu serveur ni les erreurs lancees par le boundary lui-meme.

EmplacementCapture par Error BoundaryTraitement en production
Erreur de rendu d’un enfantOuiAfficher une UI fallback et envoyer un log redacte
Erreur dans un hook ou memo pendant le renduSouvent ouiValider les echecs attendus avant, envoyer les exceptions inattendues au boundary
Clic de bouton ou submit de formulaireNonGerer avec try/catch local, puis relancer via l’etat si necessaire
setTimeout, requestAnimationFrame ou promesse normaleNonTraiter explicitement la promesse et proposer un retry
Rendu cote serveurNonUtiliser la page d’erreur du framework, les logs serveur et le statut HTTP
Erreur dans le fallback du boundaryNonGarder le fallback simple et ajouter un boundary plus haut
flowchart TD
  A["Un composant enfant lance pendant le rendu"] --> B["Error Boundary le plus proche"]
  B --> C["UI fallback pour l'utilisateur"]
  B --> D["Rapport d'erreur redacte"]
  E["Gestionnaire de clic ou setTimeout echoue"] --> F["Gerer localement ou relancer via l'etat"]
  F --> B

Separer Les Boundaries De Route Et De Composant

Multiplier les boundaries ne suffit pas a faire une bonne architecture. Un seul boundary autour de toute l’app est trop large, mais un boundary autour de chaque bouton produit du bruit et des fragments d’erreur. Demandez a Claude Code de distinguer les boundaries de route et les boundaries de composant.

Un boundary de route protege une responsabilite d’ecran : dashboard, parametres, facturation, editeur, recherche ou journal d’audit admin. Il doit se reset quand la navigation change afin qu’une erreur de l’ancienne route ne suive pas l’utilisateur sur la suivante.

Un boundary de composant protege une region independante dans la page. Les bons candidats sont un graphique de revenus, un panneau de notifications, une preview Markdown, un widget de recommandations, un embed tiers ou un visualiseur JSON lourd. Un input simple, un bouton submit, un titre ou une icone ne sont pas de bons candidats : ils relevent de la validation et de l’etat UI normal.

Posez trois questions : l’utilisateur peut-il continuer a travailler si cette region echoue ; cette region peut-elle retry, reload ou reset seule ; le nom de feature dans le log aide-t-il le diagnostic. Ce modele rejoint les strategies de test avec Claude Code : l’unite que l’utilisateur peut relancer doit etre une unite testable.

Composant Error Boundary Pret A Copier

Le boundary partage reste un class component. Le reste de l’application peut rester en function components ; seule cette enveloppe utilise les lifecycles React prevus pour les error boundaries.

// src/components/error-boundary/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from "react";

export type ErrorBoundaryFallbackProps = {
  error: Error;
  resetErrorBoundary: () => void;
};

type ErrorBoundaryProps = {
  children: ReactNode;
  fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
  onError?: (error: Error, info: ErrorInfo) => void;
  onReset?: () => void;
  resetKeys?: ReadonlyArray<unknown>;
};

type ErrorBoundaryState = {
  error: Error | null;
};

function normalizeError(value: unknown): Error {
  if (value instanceof Error) return value;
  return new Error(typeof value === "string" ? value : "Unknown render error");
}

function changedArray(
  previous: ReadonlyArray<unknown> = [],
  next: ReadonlyArray<unknown> = [],
): boolean {
  return (
    previous.length !== next.length ||
    previous.some((item, index) => !Object.is(item, next[index]))
  );
}

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  state: ErrorBoundaryState = { error: null };

  static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
    return { error: normalizeError(error) };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(normalizeError(error), info);
  }

  componentDidUpdate(previousProps: ErrorBoundaryProps) {
    if (
      this.state.error &&
      changedArray(previousProps.resetKeys, this.props.resetKeys)
    ) {
      this.resetErrorBoundary();
    }
  }

  resetErrorBoundary = () => {
    this.props.onReset?.();
    this.setState({ error: null });
  };

  render() {
    if (!this.state.error) return this.props.children;

    if (typeof this.props.fallback === "function") {
      return this.props.fallback({
        error: this.state.error,
        resetErrorBoundary: this.resetErrorBoundary,
      });
    }

    if (this.props.fallback) return this.props.fallback;

    return (
      <section role="alert" aria-labelledby="error-boundary-title">
        <h2 id="error-boundary-title">Something went wrong</h2>
        <p>Please retry. If the problem continues, contact support.</p>
        <button type="button" onClick={this.resetErrorBoundary}>
          Try again
        </button>
      </section>
    );
  }
}

fallback peut etre un noeud statique ou une fonction. La forme fonctionnelle est plus utile car elle recoit error et resetErrorBoundary. Ne montrez pas error.stack, les reponses API brutes ni les messages internes a l’utilisateur. Il lui faut une explication courte, une action sure et parfois une reference de support.

UI Fallback, Reset Et Retry

L’UI fallback n’est pas un dump de debug, c’est de l’interface produit. Elle doit dire quelle partie ne fonctionne plus, si les donnees utilisateur ont ete modifiees et quelle action est sure. Pour une erreur de chargement de chunk apres deploiement, recharger toute l’app peut aider. Pour un widget ordinaire, relancer uniquement la region est souvent mieux.

// src/components/error-boundary/AppErrorFallback.tsx
import type { ErrorBoundaryFallbackProps } from "./ErrorBoundary";

export function AppErrorFallback({
  error,
  resetErrorBoundary,
}: ErrorBoundaryFallbackProps) {
  const reloadRecommended =
    /ChunkLoadError|Loading chunk|dynamically imported module/i.test(
      error.message,
    );

  return (
    <section
      role="alert"
      aria-labelledby="app-error-title"
      className="error-fallback"
    >
      <div>
        <p className="error-fallback__eyebrow">This section stopped working</p>
        <h2 id="app-error-title">We could not render this part of the page.</h2>
        <p>
          Your account data was not changed. Retry this section first, then
          reload the app if the same message appears again.
        </p>
      </div>

      <div className="error-fallback__actions">
        <button type="button" onClick={resetErrorBoundary}>
          Try again
        </button>
        {reloadRecommended ? (
          <button type="button" onClick={() => window.location.reload()}>
            Reload app
          </button>
        ) : null}
      </div>
    </section>
  );
}
/* src/components/error-boundary/error-fallback.css */
.error-fallback {
  border: 1px solid #d7dde8;
  border-radius: 8px;
  padding: 16px;
  background: #fff;
  color: #1f2937;
}

.error-fallback__eyebrow {
  margin: 0 0 4px;
  color: #6b7280;
  font-size: 0.875rem;
}

.error-fallback h2 {
  margin: 0 0 8px;
  font-size: 1.125rem;
}

.error-fallback__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}

Le bug classique du retry consiste a effacer uniquement l’etat du boundary sans changer l’entree qui casse. Si les memes props, le meme cache ou le meme etat de route relancent la meme erreur, le bouton semble inutile. Utilisez resetKeys pour les cles de route, filtres, user id, compteurs de refresh ou versions de donnees.

Exemples Route Et Composant

Avec React Router, le boundary de route peut etre un wrapper leger. L’exemple ci-dessous reset avec location.key et envoie un nom de feature dans le log. Next.js et Remix ont leurs fichiers d’erreur de route, mais la logique reste la meme : reset a la navigation et isolation par ecran.

// src/AppRoutes.tsx
import { lazy, ReactNode, Suspense } from "react";
import {
  createBrowserRouter,
  RouterProvider,
  useLocation,
} from "react-router-dom";
import { ErrorBoundary } from "./components/error-boundary/ErrorBoundary";
import { AppErrorFallback } from "./components/error-boundary/AppErrorFallback";
import { currentErrorContext, reportReactError } from "./lib/error-reporting";
import { Layout } from "./routes/Layout";

const DashboardPage = lazy(() => import("./routes/DashboardPage"));
const SettingsPage = lazy(() => import("./routes/SettingsPage"));

function RouteBoundary({
  children,
  feature,
}: {
  children: ReactNode;
  feature: string;
}) {
  const location = useLocation();

  return (
    <ErrorBoundary
      resetKeys={[location.key]}
      fallback={(props) => <AppErrorFallback {...props} />}
      onError={(error, info) => {
        void reportReactError(
          error,
          info.componentStack,
          currentErrorContext(feature),
        );
      }}
    >
      <Suspense fallback={<p>Loading...</p>}>{children}</Suspense>
    </ErrorBoundary>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        path: "dashboard",
        element: (
          <RouteBoundary feature="dashboard">
            <DashboardPage />
          </RouteBoundary>
        ),
      },
      {
        path: "settings",
        element: (
          <RouteBoundary feature="settings">
            <SettingsPage />
          </RouteBoundary>
        ),
      },
    ],
  },
]);

export function AppRoutes() {
  return <RouterProvider router={router} />;
}

Les boundaries de composant appartiennent aux regions recuperables seules : graphiques, previews Markdown, panneaux de recommandation, embeds tiers et visualiseurs JSON. Ils ne doivent pas entourer chaque champ de formulaire. Un paiement refuse, une validation invalide ou une session expiree sont des etats produit normaux.

Erreurs Asynchrones Et Gestionnaires

Error Boundary ne capture pas automatiquement les clics ni les echecs async ordinaires. Les echecs attendus doivent rester locaux : erreurs de champs, authentification, paiement. Les exceptions inattendues peuvent etre stockees dans l’etat puis lancees au rendu suivant pour atteindre le boundary le plus proche.

// src/components/error-boundary/useAsyncBoundary.ts
import { useCallback, useState } from "react";

function toError(value: unknown): Error {
  if (value instanceof Error) return value;
  return new Error(typeof value === "string" ? value : "Unknown async error");
}

export function useAsyncBoundary() {
  const [error, setError] = useState<Error | null>(null);

  if (error) {
    throw error;
  }

  return useCallback((value: unknown) => {
    setError(toError(value));
  }, []);
}
// src/components/settings/SaveButton.tsx
import { useState } from "react";
import { useAsyncBoundary } from "../error-boundary/useAsyncBoundary";

type SaveButtonProps = {
  onSave: () => Promise<void>;
};

export function SaveButton({ onSave }: SaveButtonProps) {
  const [pending, setPending] = useState(false);
  const throwToBoundary = useAsyncBoundary();

  async function handleClick() {
    setPending(true);

    try {
      await onSave();
    } catch (error) {
      throwToBoundary(error);
    } finally {
      setPending(false);
    }
  }

  return (
    <button type="button" disabled={pending} onClick={handleClick}>
      {pending ? "Saving..." : "Save"}
    </button>
  );
}

Dites a Claude Code de ne pas envoyer tous les echecs async au boundary. Un 400, un champ invalide, une limite de debit ou une session expiree doivent avoir une UI locale. Le boundary est reserve aux exceptions inattendues, aux reponses corrompues et aux hypotheses de rendu qui casseraient la page.

Logger Sans Divulguer De PII

PII designe les informations permettant d’identifier une personne : email, telephone, nom, adresse, token, carte bancaire ou message libre de support. componentDidCatch est un bon endroit pour reporter une erreur client, mais seulement avec un payload limite et redacte.

Loggez feature, release, pathname de route, nom d’erreur, message redacte, stack et componentStack. N’envoyez pas les query strings, valeurs de formulaire, cookies, headers Authorization, reponses API brutes ni URL complete.

// src/lib/error-reporting.ts
type ClientErrorContext = {
  route: string;
  release: string;
  feature?: string;
  userHash?: string;
};

const REDACTIONS: Array<[RegExp, string]> = [
  [/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted-email]"],
  [/\b(?:\d[ -]*?){13,19}\b/g, "[redacted-number]"],
  [/\b(token|secret|password|authorization)=([^&\s]+)/gi, "$1=[redacted]"],
  [/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]"],
];

export function redactText(value: string | undefined): string | undefined {
  if (!value) return value;

  return REDACTIONS.reduce(
    (text, [pattern, replacement]) => text.replace(pattern, replacement),
    value,
  );
}

export function currentErrorContext(feature?: string): ClientErrorContext {
  const env = (import.meta as unknown as {
    env?: Record<string, string | undefined>;
  }).env;

  return {
    route: typeof window === "undefined" ? "server" : window.location.pathname,
    release: env?.VITE_APP_VERSION ?? "dev",
    feature,
  };
}

export async function reportReactError(
  error: Error,
  componentStack: string | undefined,
  context: ClientErrorContext,
) {
  const payload = {
    name: redactText(error.name) ?? "Error",
    message: redactText(error.message) ?? "Unknown error",
    stack: redactText(error.stack),
    componentStack: redactText(componentStack),
    route: context.route,
    release: context.release,
    feature: context.feature,
    userHash: context.userHash,
  };

  const body = JSON.stringify(payload);

  if (typeof navigator !== "undefined" && navigator.sendBeacon) {
    const sent = navigator.sendBeacon(
      "/api/client-errors",
      new Blob([body], { type: "application/json" }),
    );
    if (sent) return;
  }

  await fetch("/api/client-errors", {
    method: "POST",
    headers: { "content-type": "application/json" },
    credentials: "omit",
    keepalive: true,
    body,
  });
}

Redactez aussi cote serveur. La redaction client aide, mais ce n’est pas une frontiere de conformite. Demandez a Claude Code les deux couches et conservez uniquement un identifiant utilisateur deja hashe si la correlation support est necessaire.

Tests Et Commandes De Verification

Un Error Boundary compte quand quelque chose casse ; il faut donc tester le chemin casse. Le minimum : fallback affiche, onError appele, reset par retry.

// src/components/error-boundary/ErrorBoundary.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import { ReactNode, useState } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ErrorBoundary } from "./ErrorBoundary";

function Bomb({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) {
    throw new Error("profile widget crashed");
  }

  return <p>Profile loaded</p>;
}

function RetryHarness({ onError }: { onError: ReturnType<typeof vi.fn> }) {
  const [broken, setBroken] = useState(true);

  return (
    <ErrorBoundary
      onError={onError}
      fallback={({ resetErrorBoundary }) => (
        <button
          type="button"
          onClick={() => {
            setBroken(false);
            resetErrorBoundary();
          }}
        >
          Retry profile
        </button>
      )}
    >
      <Bomb shouldThrow={broken} />
    </ErrorBoundary>
  );
}

function StaticFallback({ children }: { children: ReactNode }) {
  return (
    <ErrorBoundary fallback={<p>Could not load this panel.</p>}>
      {children}
    </ErrorBoundary>
  );
}

describe("ErrorBoundary", () => {
  let consoleErrorSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  });

  afterEach(() => {
    consoleErrorSpy.mockRestore();
  });

  it("renders fallback UI when a child throws", () => {
    render(
      <StaticFallback>
        <Bomb shouldThrow />
      </StaticFallback>,
    );

    expect(screen.getByText("Could not load this panel.")).toBeInTheDocument();
  });

  it("calls onError with the thrown error and component stack", () => {
    const onError = vi.fn();

    render(<RetryHarness onError={onError} />);

    expect(onError).toHaveBeenCalledTimes(1);
    expect(onError.mock.calls[0][0].message).toBe("profile widget crashed");
    expect(onError.mock.calls[0][1].componentStack).toContain("Bomb");
  });

  it("can reset and render children again", async () => {
    const user = userEvent.setup();
    const onError = vi.fn();

    render(<RetryHarness onError={onError} />);
    await user.click(screen.getByRole("button", { name: "Retry profile" }));

    expect(screen.getByText("Profile loaded")).toBeInTheDocument();
  });
});
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
npm run typecheck
npx vitest run src/components/error-boundary/ErrorBoundary.test.tsx
npm run build

Prompts Surs Pour Claude Code

Add React Error Boundaries to this React + TypeScript app.

Constraints:
- Follow the official React Error Boundary model.
- Catch render errors from descendants, but handle event handlers and ordinary async failures separately.
- Implement a shared ErrorBoundary class, user-facing fallback UI, and reportReactError with PII redaction.
- Route-level boundaries must reset on navigation through resetKeys.
- Component-level boundaries should only wrap independent regions such as DashboardChart, MarkdownPreview, and RecommendationPanel.
- Do not log error.stack, query strings, form values, Authorization headers, cookies, or raw API responses without redaction.
- Add Vitest + Testing Library coverage for fallback UI, onError, and retry reset.
- Run npm run typecheck, npx vitest run, and npm run build, then report the results.

Read the existing routing, logging, and CSS conventions first. Keep the diff minimal.

Prompt de revue :

Review this diff only from the Error Boundary perspective.
List issues with boundary placement, async errors that are not caught, PII leakage, missing resetKeys, fallback accessibility, and missing tests.
Do not change code. Return file names and line numbers.

Cas D’Usage Et Pieges

Premier cas : un dashboard SaaS. Entourez separement le graphique de revenus, la table d’utilisateurs actifs, le panneau de notifications et l’embed tiers. Un bug de librairie graphique ne doit pas bloquer les parametres ou la facturation. Dans les logs, utilisez un nom comme dashboard.revenue-chart.

Deuxieme cas : un editeur de contenu. Preview Markdown, preview d’image et panneau de resume IA sont fragiles. L’editeur de texte et le bouton de sauvegarde sont le coeur du travail. Le preview peut avoir un boundary ; l’echec de sauvegarde doit rester dans le handler.

Troisieme cas : ecommerce ou inscription. Carte refusee, rupture de stock et validation invalide ne sont pas des erreurs de boundary, mais des etats produit attendus. Les recommandations, bannieres de campagne et widgets d’avis peuvent etre isoles.

Quatrieme cas : journal d’audit admin. Un visualiseur JSON lourd peut lancer pendant le formatage. Entourez le visualiseur, pas toute la page, afin que l’operateur puisse encore changer les filtres, exporter un CSV ou inspecter un autre utilisateur.

Les pieges frequents sont : croire qu’un try/catch autour du JSX suffit, envoyer tous les echecs async au boundary, logger l’URL complete avec query strings, afficher les stack traces, reset sans changer l’entree qui casse, et mettre tellement de petits boundaries que la page devient une mosaique de fallbacks. En equipe, transformez les prompts d’implementation et de revue en commandes Claude Code reutilisables. Pour standardiser l’approche, reliez l’article a une formation et un accompagnement Claude Code.

Resume

Un Error Boundary n’est pas un gestionnaire universel d’exceptions. C’est une frontiere React pour les erreurs de rendu, avec UI fallback et logging sur. Avec Claude Code, precisez la portee de capture, le placement route et composant, le reset, la politique PII, les tests et les commandes de verification.

Dans le test pratique sur dashboard, definir resetKeys et les regles de redaction avant de demander le code a reduit le temps de revue. L’application ne tombait plus entiere quand un widget cassait, et les logs restaient utiles sans exposer les donnees utilisateur.

#Claude Code #React #Error Boundary #gestion des erreurs #TypeScript
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.