En aplicaciones web distribuidas contemporáneas que integran docenas de widgets independientes y fuentes asíncronas de datos, el fallo de un componente secundario (como un carrusel de recomendaciones o un gráfico financiero) jamás debe provocar el colapso total de la interfaz («pantalla blanca de la muerte»).
La tolerancia arquitectónica a fallos en React se sustenta en dos mecanismos de protección declarativos:
Sin límites de contención granulares, la aplicación queda expuesta a tres vulnerabilidades críticas:
Suspense anidados fuerza a los componentes hijos a resolver datos secuencialmente en vez de en paralelo, multiplicando los tiempos de carga y generando un parpadeo visual molesto.React concibe el renderizado como una función pura y determinista. Si se produce una excepción durante la ejecución de un componente, React asciende por la jerarquía en busca del capturador de errores más cercano. Si no existe un Error Boundary en la ruta, se desmonta la aplicación al completo.
Del mismo modo que un bloque try...catch tradicional captura excepciones síncronas en código imperativo, un Error Boundary actúa como un try...catch declarativo para el árbol del DOM virtual.
Suspense funciona de modo análogo: cuando un componente accede a un recurso asíncrono mediante el hook use(Promise) de React 19, suspende su renderizado (arrojando una promesa al runtime de React), lo que instruye al límite Suspense más cercano a mostrar su loadingFallback.
El componente solicita datos con use(Promise); el límite Suspense despliega un esqueleto a medida.
Ante una excepción en renderizado, el Error Boundary atrapa el fallo, evitando la pantalla en blanco.
componentDidCatch despacha el seguimiento de la pila y metadatos a la plataforma de monitoreo.
El usuario pulsa 'Reintentar' o mutan los parámetros (resetKeys); se reinicia el estado y la caché.
¿Cuál de las siguientes tipologías de error NO es interceptada de forma automática por un Error Boundary de React?
Paneles de control corporativos con decenas de gráficos, balances financieros y tablas. Si el endpoint de una métrica devuelve error 500 o JSON truncado, solo ese bloque específico presenta el mensaje de error con un botón de reintento, dejando el 95% restante del panel plenamente operativo.
Uso de límites Suspense provistos de componentes Skeleton que emulan con total fidelidad el ancho, alto y márgenes del contenido final. Esto preserva la métrica CLS próxima a 0, impidiendo saltos visuales intempestivos cuando el contenido finaliza su carga.
En la arquitectura de React Server Components, un error en el renderizado del servidor dentro de un Error Boundary no interrumpe el flujo HTTP global: el servidor entrega la carcasa estructural de la página y la sección afectada conmuta limpiamente a su interfaz de reserva en el cliente.
El protocolo de intercepción de fallos en React se segmenta rigurosamente entre la derivación síncrona del estado y la auditoría asíncrona:
Durante el renderizado se desata un TypeError o se rechaza una promesa en el hook use().
React invoca static getDerivedStateFromError(error). El componente actualiza su estado a { hasError: true, error }.
En la fase de commit se dispara componentDidCatch(error, errorInfo), remitiendo la traza a Sentry o Datadog.
React renderiza el componente fallbackRender ofreciendo la acción de reintento (resetErrorBoundary).
¿De qué manera el array resetKeys evita que el usuario quede atrapado en una pantalla de error al navegar por el cliente?
Implementación estricta en TypeScript de un componente de clase para contención de errores sin tipos any:
'use client';
import * as React from 'react';
export interface FallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
export interface ErrorBoundaryProps {
/** Función de reserva que recibe el objeto de error y la acción de reinicio */
fallbackRender?: (props: FallbackProps) => React.ReactNode;
/** ReactNode estático de reserva si no se requiere lógica de reintento */
fallback?: React.ReactNode;
/** Callback para remitir información al servicio de monitoreo */
onError?: (error: Error, info: React.ErrorInfo) => void;
/** Array de dependencias que desencadenan el reinicio automático del error al mutar */
resetKeys?: Array<unknown>;
children: React.ReactNode;
}
interface ErrorBoundaryState {
error: Error | null;
}
const initialState: ErrorBoundaryState = { error: null };
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
public override state: ErrorBoundaryState = initialState;
public static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
const normalizedError = error instanceof Error ? error : new Error(String(error));
return { error: normalizedError };
}
public override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Despacho de la incidencia al servicio de telemetría (Sentry / Datadog)
this.props.onError?.(error, errorInfo);
}
public override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
const { error } = this.state;
const { resetKeys } = this.props;
// Reinicio automático del error si cambiaron los resetKeys
if (error !== null && resetKeys && prevProps.resetKeys) {
const hasKeysChanged = resetKeys.some(
(key, index) => !Object.is(key, prevProps.resetKeys?.[index])
);
if (hasKeysChanged) {
this.reset();
}
}
}
public reset = (): void => {
this.setState(initialState);
};
public override render(): React.ReactNode {
const { error } = this.state;
const { fallbackRender, fallback, children } = this.props;
if (error !== null) {
if (typeof fallbackRender === 'function') {
return fallbackRender({
error,
resetErrorBoundary: this.reset,
});
}
if (fallback !== undefined) {
return fallback;
}
// Interfaz de reserva por defecto
return (
<div role="alert" className="p-4 rounded-xl bg-red-950/30 border border-red-900/50 text-red-200 text-sm">
<p className="font-semibold mb-1">Se produjo un fallo en el componente</p>
<p className="text-xs text-red-400 font-mono mb-3">{error.message}</p>
<button
type="button"
onClick={this.reset}
className="px-3 py-1.5 bg-red-800/60 hover:bg-red-700/60 rounded text-xs font-medium transition-colors cursor-pointer"
>
Reintentar
</button>
</div>
);
}
return children;
}
}Integración combinada de ErrorBoundary y Suspense en un contenedor defensivo para tareas asíncronas:
'use client';
import * as React from 'react';
import { ErrorBoundary, FallbackProps } from './ErrorBoundary';
export interface AsyncBoundaryProps {
/** Función de renderizado ante errores */
errorFallback?: (props: FallbackProps) => React.ReactNode;
/** Interfaz de reserva mientras se cargan los datos (Esqueleto) */
loadingFallback: React.ReactNode;
/** Claves para el restablecimiento automático del estado de error */
resetKeys?: Array<unknown>;
/** Callback para la gestión de errores en telemetría */
onError?: (error: Error, info: React.ErrorInfo) => void;
children: React.ReactNode;
}
/**
* AsyncBoundary: Límite unificado que aísla estados pendientes y fallos asíncronos
*/
export function AsyncBoundary({
errorFallback,
loadingFallback,
resetKeys,
onError,
children,
}: AsyncBoundaryProps) {
return (
<ErrorBoundary
fallbackRender={errorFallback}
resetKeys={resetKeys}
onError={onError}
>
<React.Suspense fallback={loadingFallback}>
{children}
</React.Suspense>
</ErrorBoundary>
);
}'use client';
import * as React from 'react';
import { AsyncBoundary } from '@/components/resilience/AsyncBoundary';
// Esqueleto calibrado a las dimensiones definitivas para certificar CLS = 0
function MetricsSkeleton() {
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-border animate-pulse space-y-4">
<div className="h-4 w-32 bg-bg-elevated rounded-md" />
<div className="h-8 w-24 bg-bg-elevated rounded-md" />
<div className="h-2 w-full bg-bg-elevated rounded-full" />
</div>
);
}
// UI de reserva con mecanismo de reintento in situ
function MetricsErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-red-900/40 space-y-3">
<div className="flex items-center gap-2 text-red-400">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span className="text-sm font-semibold">Fallo al obtener métricas</span>
</div>
<p className="text-xs text-text-muted">{error.message}</p>
<button
type="button"
onClick={resetErrorBoundary}
className="text-xs px-3 py-1.5 bg-bg-elevated text-text-primary rounded-lg hover:bg-border transition-colors cursor-pointer"
>
Reintentar carga
</button>
</div>
);
}
// Componente interno con simulación de consulta
function MetricsContent({ serverId }: { serverId: string }) {
if (serverId === 'failed-node') {
throw new Error('El nodo failed-node no está disponible (503 Service Unavailable)');
}
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-border space-y-2">
<span className="text-xs font-medium text-text-secondary uppercase tracking-wider">
Uso de CPU ({serverId})
</span>
<div className="text-3xl font-bold text-text-primary">42.8%</div>
<div className="w-full bg-bg-elevated rounded-full h-2 overflow-hidden">
<div className="bg-accent-violet h-full rounded-full" style={{ width: '42.8%' }} />
</div>
</div>
);
}
export function ServerMetricsWidget({ serverId }: { serverId: string }) {
return (
<AsyncBoundary
loadingFallback={<MetricsSkeleton />}
errorFallback={MetricsErrorFallback}
resetKeys={[serverId]} // ¡Cambiar serverId restablece automáticamente el estado de error!
onError={(err) => {
console.error(`[Telemetry]: Incidencia en widget de métricas del servidor ${serverId}:`, err);
}}
>
<MetricsContent serverId={serverId} />
</AsyncBoundary>
);
}Los Error Boundaries interceptan anomalías únicamente durante la fase de renderizado, en constructores y en métodos de ciclo de vida. Las excepciones producidas dentro de setTimeout, listeners de eventos (onClick) o promesas no controladas en useEffect no serán atrapadas.
// ❌ ANTIPATRÓN: El error en el listener no llegará al ErrorBoundary
function BadComponent() {
const handleClick = async () => {
// ¡Este error NO activará el Error Boundary! Provocará un Unhandled Promise Rejection
throw new Error('Fallo en onClick');
};
return <button onClick={handleClick}>Romper</button>;
}
// ✅ RECOMENDADO: Propagar errores de manejadores mediante showBoundary de react-error-boundary
import { useErrorBoundary } from 'react-error-boundary';
function GoodComponent() {
const { showBoundary } = useErrorBoundary();
const handleClick = async () => {
try {
await api.doWork();
} catch (err) {
showBoundary(err); // Reinyecta el error al Error Boundary más próximo en el siguiente render
}
};
return <button onClick={handleClick}>Llamada segura</button>;
}Si un componente padre suspende la carga en Suspense y su hijo suspende a su vez con otra petición en un Suspense anidado, la carga de datos se serializa innecesariamente, duplicando los tiempos de respuesta. Ejecute las consultas en paralelo con Promise.all o sitúe las fronteras de espera en niveles superiores y paralelos del árbol.
Utilizar un spinner estándar de 20x20px en un contenedor que finalmente medirá 400x300px desata un grave desplazamiento acumulado del diseño (CLS). La interfaz de reserva en Suspense debe replicar las dimensiones y espaciados definitivos del componente.
Si un componente falló debido a una respuesta errónea en una librería de estado (como TanStack Query), limitarse a ejecutar resetErrorBoundary() provocará que el componente vuelva a leer el error almacenado en caché y falle de nuevo al instante. Invalide siempre la caché de la consulta (queryClient.resetQueries()) en la rutina de reintento.