Error Boundaries React com Claude Code: guia seguro de implementacao
Implemente Error Boundaries React com Claude Code: escopo, posicionamento, reset, logs seguros, testes e prompts.
A falha mais perigosa em uma aplicacao React nao e um grafico isolado quebrar. O problema real e uma excecao pequena transformar toda a interface em uma tela em branco. Se voce pedir ao Claude Code apenas “adicione tratamento de erros”, ele pode colocar alguns try/catch e ainda deixar erros de renderizacao sem isolamento.
Este guia mostra como implementar React Error Boundaries com Claude Code de forma segura. Um Error Boundary e uma fronteira no arvore de componentes: quando um descendente lança um erro inesperado durante o render, ele mostra uma UI fallback em vez de deixar a tela inteira cair. Ele nao corrige a causa, mas reduz o impacto, orienta o usuario e gera logs uteis para investigacao.
Masa testou isso em um dashboard administrativo. O primeiro prompt colocou um unico boundary ao redor de toda a app. A tela branca diminuiu, mas um grafico quebrado escondia tambem settings e billing, e os logs continham URLs com e-mails em query strings. Quando o prompt passou a especificar posicionamento, regras de reset, redacao de PII e testes, o diff gerado pelo Claude Code ficou muito mais facil de revisar.
Fixe Os Fatos Com A Documentacao Do React
Antes de pedir codigo, fixe os fatos. A referencia oficial do React para Component explica a divisao: static getDerivedStateFromError muda o estado para mostrar fallback, e componentDidCatch trata efeitos colaterais como logging. A documentacao oficial do lint error-boundaries tambem deixa claro que try/catch em volta de JSX nao e a ferramenta certa para erros de render.
A regra pratica e simples: Error Boundary nao captura tudo. Ele captura erros inesperados lançados por componentes descendentes durante renderizacao, lifecycles ou codigo executado no fluxo de render. Ele nao captura click handlers, timers, promises comuns, server-side rendering nem erros lançados pelo proprio boundary.
| Local do erro | Capturado pelo Error Boundary | Tratamento em producao |
|---|---|---|
| Erro ao renderizar componente filho | Sim | Mostrar fallback UI e enviar log redigido |
| Erro em hook ou memo durante render | Geralmente sim | Validar falhas esperadas antes; mandar excecoes inesperadas ao boundary |
| Clique de botao ou submit de formulario | Nao | Usar try/catch local e, se necessario, relancar via estado |
setTimeout, requestAnimationFrame ou promise normal | Nao | Tratar a promise explicitamente e oferecer retry |
| Renderizacao no servidor | Nao | Usar pagina de erro do framework, logs de servidor e status HTTP |
| Erro no fallback do boundary | Nao | Manter fallback simples e adicionar boundary superior |
flowchart TD
A["Componente filho lança durante render"] --> B["Error Boundary mais proximo"]
B --> C["Fallback UI para o usuario"]
B --> D["Relatorio de erro redigido"]
E["Click handler ou setTimeout falha"] --> F["Tratar localmente ou relancar via estado"]
F --> B
Separe Boundaries De Rota E De Componente
Mais boundaries nao significam uma arquitetura melhor. Um boundary ao redor de toda a app e amplo demais, mas envolver cada botao cria ruido e mensagens fragmentadas. Peça ao Claude Code para diferenciar route-level boundaries e component-level boundaries.
Um boundary de rota protege uma responsabilidade de tela: dashboard, settings, billing, editor, busca ou audit log administrativo. Ele deve resetar quando a navegacao muda, para que uma falha da rota anterior nao acompanhe o usuario para a proxima.
Um boundary de componente protege uma regiao independente dentro da pagina. Bons candidatos sao grafico de receita, painel de notificacoes, preview Markdown, widget de recomendacao, embed de terceiros ou JSON viewer pesado. Inputs comuns, botoes de envio, titulos e icones nao sao bons candidatos; eles devem ser tratados por validacao e estado normal de UI.
Use tres perguntas: se esta regiao falhar, o usuario ainda consegue trabalhar; a regiao consegue retry, reload ou reset sozinha; o nome de feature no log ajuda o diagnostico. Isso se conecta a estrategias de teste com Claude Code: a unidade que o usuario pode tentar novamente deve ser tambem uma unidade testavel.
Componente Error Boundary Copiavel
O boundary compartilhado ainda e um class component. O restante da app pode continuar com function components; somente esta camada usa os lifecycle methods que o React disponibiliza para 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 pode ser um node estatico ou uma funcao. A forma funcional e mais util porque recebe error e resetErrorBoundary. Mesmo assim, nao mostre error.stack, respostas API completas nem mensagens internas para o usuario. A tela precisa de uma explicacao curta, uma acao segura e, no maximo, uma referencia de suporte.
Fallback UI, Reset E Retry
Fallback UI nao e dump de debug; e interface de produto. Ela deve explicar qual parte parou, se os dados do usuario foram alterados e qual acao e segura. Para erro de chunk loading depois de deploy, recarregar a app pode ajudar. Para um widget comum, tentar novamente apenas a regiao e melhor.
// 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;
}
O bug comum no retry e resetar apenas o estado do boundary e deixar intacta a entrada quebrada. Se as mesmas props, o mesmo cache ou o mesmo estado de rota lançam novamente, o botao parece nao funcionar. Use resetKeys para route key, filtros, user id, contador de refresh ou versao de dados.
Exemplos De Rota E Componente
Com React Router, o boundary de rota pode ser um wrapper fino. O exemplo abaixo reseta com location.key e envia o nome da feature no log. Next.js e Remix oferecem arquivos de erro por rota, mas a decisao de desenho e a mesma: resetar na navegacao e isolar falhas por tela.
// 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} />;
}
Boundaries de componente pertencem a regioes que podem se recuperar sozinhas: graficos, previews Markdown, paineis de recomendacao, embeds de terceiros e JSON viewers. Eles nao pertencem a cada campo de formulario. Pagamento recusado, validacao falha ou sessao expirada sao estados normais de produto.
Falhas Async E Event Handlers
Error Boundary nao captura automaticamente clicks nem falhas async comuns. Falhas esperadas devem ficar na UI local: validacao de campo, autenticacao, pagamento. Excecoes inesperadas podem ser guardadas em estado e lançadas no proximo render para chegar ao boundary mais proximo.
// 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>
);
}
Escreva no prompt que nem toda falha async deve ir para o boundary. Um 400, campo invalido, rate limit ou sessao expirada precisam de UI local. O boundary e para excecoes inesperadas, respostas corrompidas e suposicoes de render que poderiam derrubar a tela.
Logs Sem Vazar PII
PII significa informacao que identifica uma pessoa: e-mail, telefone, nome, endereco, tokens, cartao ou texto livre de suporte. componentDidCatch e um bom ponto para reportar erro do cliente, mas o payload precisa ser limitado e redigido.
Registre feature, release, route pathname, nome do erro, mensagem redigida, stack e componentStack. Nao envie query strings, valores de formulario, cookies, Authorization headers, respostas API brutas nem URL completa.
// 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,
});
}
Redija novamente no servidor. Redacao no cliente ajuda, mas nao e uma fronteira de conformidade. Peça ao Claude Code as duas camadas e use apenas identificadores de usuario ja hasheados quando precisar de correlacao com suporte.
Testes E Comandos De Verificacao
Um Error Boundary so importa quando algo quebra, entao teste o caminho quebrado. O minimo e fallback renderizado, onError chamado e reset por 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 Seguros Para 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 revisao:
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.
Casos De Uso E Armadilhas
Primeiro caso: dashboard SaaS. Envolva separadamente grafico de receita, tabela de usuarios ativos, painel de notificacoes e embed externo. Um bug da biblioteca de graficos nao deve bloquear settings ou billing. Nos logs, use nomes como dashboard.revenue-chart.
Segundo caso: editor de conteudo. Preview Markdown, preview de imagem e painel de resumo com IA sao regioes frageis. Editor de texto e botao salvar sao superficies principais. O preview pode ter boundary; falha ao salvar deve ficar no event handler.
Terceiro caso: ecommerce ou inscricao. Cartao recusado, falta de estoque e validacao falha nao sao erros de boundary, mas estados esperados do produto. Modulos de recomendacao, banners de campanha e widgets de reviews podem ser isolados.
Quarto caso: audit log administrativo. Um JSON viewer grande pode lançar erro durante formatacao. Envolva o viewer, nao a pagina inteira, para que o operador continue mudando filtros, exportando CSV ou verificando outro usuario.
Armadilhas comuns: confiar em try/catch ao redor de JSX, mandar todas as falhas async ao boundary, logar URL completa com query strings, mostrar stack trace na UI, resetar sem mudar a entrada quebrada e criar tantos boundaries pequenos que a pagina vira uma colecao de fallbacks. Para times, transforme os prompts de implementacao e revisao em comandos reutilizaveis do Claude Code. Se precisar padronizar isso em uma base de codigo, conecte o proximo passo a treinamento e suporte de implementacao Claude Code.
Resumo
Um Error Boundary nao e um handler universal de excecoes. Ele e uma fronteira especifica do React para falhas de render, com fallback UI e logging seguro. Ao usar Claude Code, especifique escopo de captura, posicionamento por rota e componente, comportamento de reset, politica de PII, testes e comandos de verificacao.
No teste pratico do dashboard, definir resetKeys e regras de redacao antes de pedir o codigo reduziu o tempo de revisao. A app deixou de cair inteira quando um widget quebrava, e os logs continuaram uteis sem expor dados de usuarios.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Permission receipt no Claude Code: escopo, prova e rollback
Padrão de permission receipt para Claude Code: ações permitidas, limites de aprovação, comandos de prova, rollback e CTA de receita.
Agent Harness seguro para Claude Code e Codex: permissoes, verificacao e rollback
Monte uma base segura para agentes com Claude Code e Codex usando politicas, plano, verificacao e recuperacao.
Subagentes no Claude Code: guia prático para delegar trabalho com segurança
Guia prático de subagentes no Claude Code para dividir artigos e código: regras, prompts, riscos e checklist.