Error boundaries y manejo de errores en React
¿Por qué necesitamos Error Boundaries?
En las aplicaciones React modernas, los errores en un componente pueden propagarse y destruir toda la interfaz de usuario. Imagina que un pequeño componente de botón falla: sin Error Boundaries, toda tu aplicación se volvería inoperable. Los Error Boundaries actúan como "contenedores de seguridad" que aíslan los errores y mantienen funcionando el resto de la aplicación.
¿Qué pueden y qué no pueden capturar?
Es crucial entender las limitaciones de los Error Boundaries:
| Pueden capturar | No pueden capturar |
|---|---|
| Errores en renderización | Eventos (onClick, onChange, etc.) |
| Métodos del ciclo de vida | Código asíncrono (setTimeout, Promises) |
| Constructores de componentes | Errores fuera del árbol de componentes |
| Errores en hooks personalizados | El propio Error Boundary |
Implementación de un Error Boundary
Para crear un Error Boundary, necesitas una clase de componente React que implemente uno o ambos de estos métodos estáticos:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Actualiza el estado para mostrar la UI de respaldo
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Puedes registrar el error en un servicio externo
console.error('Error capturado:', error, errorInfo);
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Algo salió mal</h2>
<details>
<summary>Ver detalles del error</summary>
<p>{this.state.error && this.state.error.toString()}</p>
</details>
</div>
);
}
return this.props.children;
}
}Uso práctico de Error Boundaries
Ahora veamos cómo aplicar el Error Boundary en tu aplicación:
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import Header from './Header';
import Content from './Content';
import Sidebar from './Sidebar';
function App() {
return (
<div className="app">
<Header />
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<Content />
</ErrorBoundary>
);
}
// También puedes crear múltiples Error Boundaries específicos
function Dashboard() {
return (
<ErrorBoundary fallback={
</ErrorBoundary>
);
}
Manejo de errores en código asíncrono
Los Error Boundaries NO capturan errores en manejadores de eventos, callbacks asíncronos o Promises. Para estos casos, necesitas usar try-catch tradicional:
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Usuario no encontrado');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
}
}
fetchUser();
}, [userId]);
if (error) return <ErrorMessage message={error} />;
if (!user) return <LoadingSpinner />;
return <Profile user={user} />;
}Patrones avanzados de Error Boundaries
1. Error Boundary con fallback personalizado
function ErrorBoundaryWithFallback({ fallback, children }) {
const [error, setError] = React.useState(null);
React.useEffect(() => {
const errorHandler = (event) => {
setError(event.error);
event.preventDefault();
};
window.addEventListener('error', errorHandler);
return () => window.removeEventListener('error', errorHandler);
}, []);
if (error) {
return fallback({ error, reset: () => setError(null) });
}
return children;
}
// Uso:
<ErrorBoundaryWithFallback
fallback={({ error, reset }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Reintentar</button>
</div>
)}
>
<ComponenteInestable />
</ErrorBoundaryWithFallback>2. Error Boundary con niveles de recuperación
class RecoveryErrorBoundary extends React.Component {
state = { retryCount: 0 };
static getDerivedStateFromError(error) {
return { hasError: true };
}
handleRetry = () => {
this.setState(prev => ({
hasError: false,
retryCount: prev.retryCount + 1
}));
};
render() {
if (this.state.hasError) {
return (
<div className="recovery-ui">
<h3>⚠️ Componente temporalmente no disponible</h3>
<p>Intentos: {this.state.retryCount}</p>
{this.state.retryCount < 3 ? (
<button onClick={this.handleRetry}>
Reintentar automáticamente
</button>
) : (
<p>Por favor, recarga la página</p>
)}
</div>
);
}
return this.props.children;
}
}Estrategias de monitoreo y logging
Para aplicaciones en producción, es esencial registrar los errores capturados:
// Servicio de logging centralizado
const errorLoggingService = {
log(error, errorInfo) {
// Enviar a tu backend
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
}),
});
},
// Integración con servicios externos
logToSentry(error, errorInfo) {
// Sentry.captureException(error, { extra: errorInfo });
},
logToBugsnag(error, errorInfo) {
// Bugsnag.notify(error, report => {
// report.metadata = { errorInfo };
// });
},
};
class ProductionErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
errorLoggingService.log(error, errorInfo);
if (process.env.NODE_ENV === 'production') {
errorLoggingService.logToSentry(error, errorInfo);
}
}
render() {
return this.props.children;
}
}Integración con React Router
Un patrón común es combinar Error Boundaries con React Router para manejar errores de navegación:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<ErrorBoundary>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
<ErrorBoundary fallback={
<Dashboard />
</ErrorBoundary>
}
/>
<Route path="/users" element={
<ErrorBoundary fallback={
<UserList />
</ErrorBoundary>
} />
</Routes>
</ErrorBoundary>
</BrowserRouter>
);
}Ver más: Ejemplo completo de Error Boundary reutilizable
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.props.onError?.(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback({
error: this.state.error,
errorInfo: this.state.errorInfo,
resetError: this.resetError,
});
}
return (
<div className="error-boundary" role="alert">
<h2>Error</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.resetError}>Reintentar</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
// Componente wrapper para uso con hooks
export function withErrorBoundary(
Component,
fallback
) {
return function WrappedComponent(props) {
return (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
);
};
}Mejores prácticas
- Aísla los errores: Coloca Error Boundaries en niveles estratégicos de tu aplicación, no envolver todo.
- Proporciona contexto: Muestra mensajes útiles al usuario sin exponer detalles técnicos sensibles.
- Implementa logging: Envía los errores a un servicio de monitoreo para poder depurarlos.
- Ofrece recuperación: Incluye opciones para reintentar o navegar a un estado seguro.
- Prueba tus Error Boundaries: Simula errores intencionalmente para verificar que funcionan correctamente.
Los Error Boundaries son como los airbags de tu aplicación: esperas no necesitarlos, pero cuando ocurre un error, son la diferencia entre una recuperación suave y un fracaso total.
Ejercicio práctico
Implementa un Error Boundary que:
- Muestre un mensaje amigable cuando un componente falla
- Tenga un botón "Reintentar" que recupere el componente
- Registre los errores en la consola
- Limit los reintentos a 3 intentos antes de mostrar un mensaje de "contactar soporte"
¿Cuál de los siguientes errores SÍ puede capturar un Error Boundary?
- A) Errores en manejadores onClick
- B) Errores en Promises con await
- C) Errores durante el renderizado de un componente
- D) Errores en setTimeout
Conclusión
Los Error Boundaries son una herramienta esencial para crear aplicaciones React robustas y profesionales. Combinados con manejo de errores tradicional en código asíncrono y sistemas de logging externos, te permiten crear experiencias de usuario resilientes donde incluso cuando algo falla, la aplicación puede recuperarse elegantemente. Practica implementando Error Boundaries en diferentes niveles de tu aplicación y siempre prueba sus comportamientos bajo condiciones de error.