Manejo de errores
Receta
SWR captura los errores lanzados por tu fetcher y los expone mediante el valor de retorno error. Configura reintentos automáticos, callbacks de error e integra con React Error Boundaries para un manejo robusto de errores.
"use client";
import useSWR from "swr";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error("Ocurrió un error al obtener los datos.");
(error as any).info = await res.json();
(error as any).status = res.status;
throw error;
}
return res.json();
};
function UserProfile({ id }: { id: string }) {
const { data, error } = useSWR(`/api/users/${id}`, fetcher, {
onError: (err) => console.error("SWR error:", err),
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 5000,
});
if (error) return <div>Error: {error.message}</div>;
return <div>{data?.name}</div>;
}Ejemplo funcional
"use client";
import useSWR from "swr";
import { Component, ReactNode } from "react";
// Clase de error personalizada con contexto adicional
class ApiError extends Error {
status: number;
info: Record<string, unknown>;
constructor(message: string, status: number, info: Record<string, unknown>) {
super(message);
this.status = status;
this.info = info;
}
}
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const info = await res.json().catch(() => ({}));
throw new ApiError(
`API error: ${res.statusText}`,
res.status,
info
);
}
return res.json();
};
// Componente Error Boundary
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
function OrderDetails({ orderId }: { orderId: string }) {
const { data, error, isLoading } = useSWR(`/api/orders/${orderId}`, fetcher, {
shouldRetryOnError: (err: ApiError) => err.status !== 404,
errorRetryCount: 3,
errorRetryInterval: 2000,
onError: (err: ApiError) => {
if (err.status === 401) {
window.location.href = "/login";
}
},
});
if (isLoading) return <div>Cargando pedido...</div>;
if (error) {
if (error instanceof ApiError && error.status === 404) {
return <div>Pedido no encontrado</div>;
}
return <div>Algo salió mal: {error.message}</div>;
}
return (
<div>
<h2>Pedido #{data.id}</h2>
<p>Estado: {data.status}</p>
<p>Total: ${data.total}</p>
</div>
);
}
export default function OrderPage({ orderId }: { orderId: string }) {
return (
<ErrorBoundary fallback={<div>Algo salió mal</div>}>
<OrderDetails orderId={orderId} />
</ErrorBoundary>
);
}Profundización
Cómo funciona
- SWR captura cualquier error lanzado o promesa rechazada del fetcher y lo establece como valor de
error. - El reintento de errores está habilitado por defecto. SWR usa backoff exponencial: reintenta a 1s, 2s, 4s, 8s, etc., limitado por
errorRetryInterval. shouldRetryOnErrorpuede sertrue,falseo una función(err) => booleanpara lógica de reintento condicional.errorRetryCountlimita el número total de intentos de reintento (por defecto: ilimitado en conexiones lentas).- El callback
onError(err, key, config)se dispara en cada error, incluidos los reintentos. - Los datos exitosos anteriores se conservan en
dataincluso cuando una revalidación falla. Esto significa quedatayerrorpueden estar definidos simultáneamente. onErrorRetryofrece control total sobre el comportamiento de reintento, incluido el tiempo y la lógica de aborto.
Variaciones
Reintento personalizado con backoff:
const { data } = useSWR("/api/data", fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Nunca reintentar en 404
if (error.status === 404) return;
// Detener tras 5 reintentos
if (retryCount >= 5) return;
// Backoff exponencial
setTimeout(() => revalidate({ retryCount }), Math.min(1000 * 2 ** retryCount, 30000));
},
});Manejador global de errores:
<SWRConfig
value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
reportToSentry(error, { key });
}
},
}}
>
{children}
</SWRConfig>Estado de error con datos obsoletos:
function Dashboard() {
const { data, error, isValidating } = useSWR("/api/stats", fetcher);
return (
<div>
{error && (
<div className="bg-yellow-100 p-2">
No se pudo actualizar. Mostrando los últimos datos conocidos.
{isValidating && " Reintentando..."}
</div>
)}
{data && <StatsDisplay stats={data} />}
</div>
);
}Notas de TypeScript
- Pasa el tipo de error como segundo genérico:
useSWR<Data, Error>(key, fetcher). - Las clases de error personalizadas te dan acceso con tipos seguros a campos adicionales.
const { data, error } = useSWR<User, ApiError>("/api/me", fetcher);
if (error) {
// error está tipado como ApiError
console.log(error.status); // number
console.log(error.info); // Record<string, unknown>
}Errores comunes
- Si tu fetcher no lanza errores en respuestas distintas de 2xx, SWR nunca establecerá
error. Valida siempreres.oken tu fetcher. datayerrorpueden ser ambos truthy al mismo tiempo. Esto ocurre cuando una revalidación falla pero existen datos en caché. No asumas que son mutuamente excluyentes.- El reintento de errores está habilitado por defecto sin límite máximo. Un endpoint que falla permanentemente reintentará indefinidamente a menos que establezcas
errorRetryCount. onErrorse dispara en cada evento de error, incluidos los reintentos, lo que puede saturar los servicios de reporte de errores. Aplica debounce o deduplica en tu manejador.- Los React Error Boundaries capturan errores de renderizado, no errores asíncronos. Los errores de SWR son asíncronos y no serán capturados por Error Boundaries a menos que los relances durante el renderizado.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Valor de retorno error de SWR | Declarativo, por componente | Hay que manejarlo en cada componente |
| Error Boundary | Captura errores en tiempo de renderizado | No captura errores asíncronos de SWR de forma nativa |
| Callback global onError | Seguimiento centralizado de errores | No puede afectar el renderizado de componentes individuales |
| Notificaciones toast | Feedback no bloqueante para el usuario | El usuario podría pasar por alto la notificación |
FAQs
¿Cómo funciona el reintento de errores por defecto de SWR?
SWR reintenta con backoff exponencial: 1s, 2s, 4s, 8s, etc., limitado por errorRetryInterval. El reintento está habilitado por defecto sin límite máximo a menos que establezcas errorRetryCount.
¿Cómo evito reintentos para códigos de estado HTTP específicos como 404?
const { data } = useSWR("/api/data", fetcher, {
shouldRetryOnError: (err) => err.status !== 404,
});O usa onErrorRetry para control total sobre la lógica de reintento por tipo de error.
Trampa: ¿Pueden data y error estar definidos al mismo tiempo?
Sí. Cuando una revalidación falla pero existen datos en caché, tanto data como error son truthy. No asumas que son mutuamente excluyentes. Muestra datos obsoletos con un banner de error para la mejor experiencia de usuario.
¿Por qué mi React Error Boundary no captura los errores de SWR?
Los Error Boundaries capturan errores durante el renderizado, no errores asíncronos. Los errores de SWR son asíncronos y no se propagarán a Error Boundaries a menos que los relances durante el renderizado o uses el modo suspense: true.
¿Cómo creo una clase de error personalizada para un mejor manejo de errores?
class ApiError extends Error {
status: number;
info: Record<string, unknown>;
constructor(message: string, status: number, info: Record<string, unknown>) {
super(message);
this.status = status;
this.info = info;
}
}Lánzala en tu fetcher cuando res.ok sea false.
¿Cómo configuro un manejador global de errores para todos los hooks de SWR?
<SWRConfig
value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
reportToSentry(error, { key });
}
},
}}
>
{children}
</SWRConfig>Trampa: ¿Por qué onError se dispara repetidamente cuando el reintento está habilitado?
onError se dispara en cada evento de error, incluido cada intento de reintento. Esto puede saturar los servicios de reporte de errores. Aplica debounce o deduplica los reportes de error en tu manejador, o limita los reintentos con errorRetryCount.
¿Qué ocurre si mi fetcher no lanza errores en respuestas distintas de 2xx?
SWR nunca establecerá error. El cuerpo de la respuesta se trata como datos exitosos. Comprueba siempre res.ok en tu fetcher y lanza un error para códigos de estado distintos de 2xx.
¿Cómo tipifico el error en useSWR con TypeScript?
Pasa el tipo de error como segundo genérico:
const { data, error } = useSWR<User, ApiError>("/api/me", fetcher);
if (error) {
console.log(error.status); // tipado como number
}Si omites el genérico de error, el valor por defecto es any.
¿Cómo puedo mostrar datos obsoletos con un banner de error cuando falla la revalidación?
- Comprueba tanto
datacomoerroren tu componente. - Si ambos existen, renderiza los datos con un mensaje de advertencia.
- Usa
isValidatingpara mostrar un indicador de "Reintentando...".