Error Boundaries de React con Claude Code: guia segura de implementacion
Implementa Error Boundaries de React con Claude Code: alcance, ubicacion, reset, logs seguros, tests y prompts.
El fallo mas peligroso en una aplicacion React no es que se rompa un grafico aislado. Lo grave es que una excepcion pequena convierta toda la interfaz en una pantalla en blanco. Si le pides a Claude Code “agrega manejo de errores” sin mas contexto, puede insertar algunos try/catch y dejar sin cubrir los errores que ocurren durante el renderizado.
Esta guia explica como implementar React Error Boundaries con Claude Code de forma segura. Un Error Boundary es una frontera dentro del arbol de componentes: cuando un descendiente lanza un error inesperado durante el renderizado, muestra una interfaz fallback en lugar de dejar que se caiga toda la vista. No corrige el bug original, pero limita el dano, da al usuario una accion clara y deja un registro util para investigacion.
Masa lo probo en un panel administrativo. El primer prompt envolvio toda la app con un unico boundary. Evito la pantalla blanca, pero un grafico roto ocultaba tambien ajustes y facturacion, y los logs contenian URLs con correos en query strings. Cuando el prompt empezo a incluir ubicacion, reglas de reset, redaccion de PII y pruebas, el resultado de Claude Code fue mucho mas facil de revisar.
Fija Los Hechos Con La Documentacion De React
Antes de pedir codigo, deja claros los hechos. La referencia oficial de React para Component explica el reparto de responsabilidades: static getDerivedStateFromError cambia el estado para mostrar fallback, y componentDidCatch sirve para efectos secundarios como logging. La documentacion oficial del lint error-boundaries tambien aclara que envolver JSX con try/catch normal no es la manera correcta de capturar errores de render.
La restriccion practica es importante: Error Boundary no captura todo. Captura errores inesperados lanzados por componentes descendientes durante renderizado, lifecycles o codigo ejecutado como parte del render. No captura click handlers, timers, promesas normales rechazadas, server-side rendering ni errores lanzados por el propio boundary.
| Lugar del fallo | Lo captura Error Boundary | Tratamiento en produccion |
|---|---|---|
| Error al renderizar un hijo | Si | Mostrar fallback UI y enviar un log redactado |
| Error en hook o memo usado durante render | Normalmente si | Validar fallos esperados antes; enviar excepciones inesperadas al boundary |
| Click de boton o submit de formulario | No | Usar try/catch local y, si hace falta, relanzar por estado |
setTimeout, requestAnimationFrame o promesa normal | No | Manejar la promesa explicitamente y ofrecer retry |
| Renderizado en servidor | No | Usar pagina de error del framework, logs de servidor y estado HTTP |
| Fallo dentro del fallback del boundary | No | Mantener el fallback simple y poner un boundary superior |
flowchart TD
A["Un componente hijo lanza durante render"] --> B["Error Boundary mas cercano"]
B --> C["Fallback UI para usuario"]
B --> D["Reporte de error redactado"]
E["Click handler o setTimeout falla"] --> F["Manejar localmente o relanzar por estado"]
F --> B
Separa Boundaries De Ruta Y De Componente
Mas boundaries no significa mejor arquitectura. Un unico boundary alrededor de toda la app es demasiado amplio; envolver cada boton genera ruido y mensajes fragmentados. Pide a Claude Code que distinga route-level boundaries y component-level boundaries.
Un boundary de ruta protege una responsabilidad de pantalla: dashboard, ajustes, facturacion, editor, busqueda o auditoria administrativa. Debe resetearse cuando cambia la navegacion, para que un fallo de la ruta anterior no siga visible en la siguiente.
Un boundary de componente protege una region independiente dentro de una pagina. Buenos candidatos son un grafico de ingresos, un panel de notificaciones, una vista previa Markdown, un widget de recomendaciones, un embed de terceros o un visor JSON pesado. Malos candidatos son un input normal, un boton de envio, un titulo o un icono. Esos casos pertenecen a validacion o estado de UI normal.
Usa tres preguntas: si esta region falla, el usuario puede seguir trabajando; la region puede retry, reload o reset por separado; el nombre de feature en el log ayuda a diagnosticar. Este criterio encaja con estrategias de testing con Claude Code: la unidad que el usuario puede reintentar deberia ser tambien una unidad que puedes probar.
Componente Error Boundary Copiable
El boundary compartido sigue siendo un class component. El resto de la aplicacion puede seguir usando function components; solo esta capa usa los lifecycle methods que React expone para errores.
// 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 puede ser un nodo estatico o una funcion. La funcion es mas practica porque recibe error y resetErrorBoundary. Aun asi, no muestres error.stack, respuestas API completas ni mensajes internos en pantalla. El usuario necesita una explicacion breve, una accion segura y, como mucho, una referencia para soporte.
Fallback UI, Reset Y Retry
La fallback UI no es un volcado de debug; es interfaz de producto. Debe explicar que parte dejo de funcionar, si los datos del usuario cambiaron y que accion es segura. Para fallos de carga de chunks tras un deploy, recargar la app puede ayudar. Para un widget normal, reintentar solo esa region suele ser mejor.
// 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;
}
El bug comun de retry es limpiar solo el estado del boundary, dejando intacta la entrada rota. Si las mismas props, cache o estado de ruta vuelven a lanzar, el boton parecera no funcionar. Usa resetKeys para claves de ruta, filtros, user id, contador de refresh o version de datos.
Ejemplos De Ruta Y Componente
Con React Router, el boundary de ruta puede ser un wrapper delgado. Este ejemplo resetea con location.key y reporta un feature name. Frameworks como Next.js y Remix ofrecen archivos de error por ruta, pero la decision de diseno es la misma: resetear al navegar y aislar fallos por pantalla.
// 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} />;
}
Los boundaries de componente pertenecen a regiones recuperables de forma independiente: graficos, preview Markdown, paneles de recomendacion, embeds de terceros y visores JSON. No pertenecen a cada campo de formulario. Un pago rechazado, una validacion fallida o una sesion expirada son estados normales del producto.
Fallos Asincronos Y Handlers
Error Boundary no captura automaticamente click handlers ni fallos async normales. Los fallos esperados deben quedarse en la UI local: errores de campo, autenticacion o pagos. Las excepciones inesperadas pueden guardarse en estado y lanzarse durante el siguiente render para que las reciba el boundary mas cercano.
// 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>
);
}
Indica a Claude Code que no envie todo fallo async al boundary. Un 400, un campo invalido, rate limit o sesion expirada deben tener UI local. El boundary es para excepciones inesperadas, respuestas corruptas y supuestos de render que romperian la pantalla.
Logs Sin Filtrar PII
PII significa informacion que puede identificar a una persona: correo, telefono, nombre, direccion, tokens, numeros de tarjeta o texto libre de soporte. componentDidCatch es buen lugar para reportar errores, pero solo con payload limitado y redactado.
Registra feature, release, pathname de ruta, nombre de error, mensaje redactado, stack y componentStack. No envies query strings, valores de formulario, cookies, Authorization headers, respuestas API completas ni la 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,
});
}
Vuelve a redactar en el servidor. La redaccion del cliente ayuda, pero no es una frontera de cumplimiento. Pide a Claude Code ambos niveles y usa solo identificadores de usuario ya hasheados.
Tests Y Comandos De Verificacion
Un Error Boundary solo importa cuando algo se rompe, asi que prueba la ruta rota. El minimo es fallback visible, llamada a onError y 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.
Y un prompt de revision:
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 Y Errores Frecuentes
Primer caso: dashboard SaaS. Envuelve por separado grafico de ingresos, tabla de usuarios activos, panel de notificaciones y embed externo. Un bug de la libreria de graficos no debe bloquear ajustes ni facturacion. En logs usa nombres como dashboard.revenue-chart.
Segundo caso: editor de contenido. Markdown preview, preview de imagenes y panel de resumen con IA son zonas fragiles. El editor de texto y el boton de guardado son superficie principal de trabajo. El preview puede tener boundary; el fallo al guardar debe mostrarse en el handler.
Tercer caso: ecommerce o formulario de registro. Tarjeta rechazada, falta de stock y validacion fallida no son errores de boundary, sino estados esperados del producto. Los modulos de recomendacion, banners de campana y widgets de reviews si pueden aislarse.
Cuarto caso: pantalla administrativa de auditoria. Un visor JSON grande puede lanzar durante el formateo. Envuelve el visor, no toda la pagina, para que el operador pueda cambiar filtros, exportar CSV o revisar otro usuario.
Los errores frecuentes son envolver JSX con try/catch, mandar todos los fallos async al boundary, registrar URLs completas con query strings, mostrar stack traces en UI, resetear sin cambiar la entrada rota y crear tantos boundaries pequenos que la pagina se llena de fallbacks. Para un equipo, convierte los prompts en comandos reutilizables de Claude Code. Si necesitas estandarizarlo en una base de codigo, enlazalo con formacion y soporte de Claude Code.
Resumen
Un Error Boundary no es un manejador universal de excepciones. Es una frontera especifica de React para errores de render, con fallback UI y logging seguro. Al usar Claude Code, especifica alcance de captura, ubicacion por ruta y por componente, reset, politica de PII, tests y comandos de verificacion.
En la prueba practica del dashboard, definir resetKeys y reglas de redaccion antes de pedir codigo redujo el tiempo de revision. La app dejo de caer completa cuando fallaba un widget, y los logs siguieron siendo utiles sin exponer datos de usuarios.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.