Next.js
Manejo de errores, resiliencia y recuperación
Explica cómo distinguir resultados esperados y fallos inesperados, diseñar boundaries, timeouts, retries, degradación y recuperación en Next.js.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo distinguir resultados esperados y fallos inesperados, diseñar boundaries, timeouts, retries, degradación y recuperación en Next.js.
Una aplicación resiliente distingue resultados esperados, fallos inesperados y dependencias degradadas. Next.js aporta error.tsx, global-error.tsx, not-found.tsx, status helpers y boundaries de Suspense, pero la recuperación real también necesita timeouts, retries, idempotencia, logging y estados de producto coherentes.
input/result
├─ expected outcome
│ ├─ empty
│ ├─ invalid
│ ├─ not found
│ ├─ unauthorized/forbidden
│ └─ conflict
└─ unexpected failure
├─ dependency unavailable
├─ invariant violated
├─ bug
└─ infrastructure timeoutNo lances excepciones para cada estado normal ni conviertas todo error en un string genérico.
app/dashboard/
├─ error.tsx
├─ loading.tsx
├─ not-found.tsx
├─ layout.tsx
└─ page.tsxerror.tsx debe ser Client Component:
"use client";
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
reportClientBoundaryError(error);
}, [error]);
return (
<section role="alert">
<h2>No fue posible cargar el panel</h2>
<p>Conservamos el resto de la aplicación. Intenta nuevamente.</p>
<button type="button" onClick={reset}>
Reintentar
</button>
</section>
);
}reset() intenta renderizar de nuevo el segmento. No limpia automáticamente una caché, corrige la dependencia ni garantiza éxito. Si el error proviene de una entrada cacheada corrupta, debes invalidarla o corregir la fuente.
La boundary captura errores de descendientes, no del layout situado en el mismo segmento. Para cubrir ese layout, coloca error.tsx en el padre.
Parent error boundary
└─ Child layout
└─ Child pageDiseña boundaries alrededor de unidades que pueden fallar independientemente: editor, analytics, pagos, perfil.
Cubre fallos del root layout. Debe incluir su propio <html> y <body> porque reemplaza el documento:
"use client";
export default function GlobalError({ error }: { error: Error }) {
return (
<html lang="es">
<body>
<main>
<h1>La aplicación no pudo iniciarse</h1>
<a href="/">Volver al inicio</a>
</main>
</body>
</html>
);
}Mantén la implementación mínima; providers y estilos del root pueden ser precisamente lo que falló.
const product = await getProduct(slug);
if (!product) notFound();notFound() interrumpe la rama y selecciona not-found.tsx cercano. No captures esa excepción con un catch genérico.
Distingue:
Solo las primeras/terceras según política son not found.
Next.js incorpora helpers/conventions de unauthorized/forbidden bajo estados que debes confirmar para la versión/configuración utilizada; algunas capacidades siguen experimentales.
No construyas toda la arquitectura alrededor de una API experimental sin fallback. Puedes usar redirects, resultados tipados o error pages propias.
type UpdateProfileState =
| { status: "idle" }
| { status: "invalid"; errors: FieldErrors }
| { status: "conflict"; message: string }
| { status: "success" };La Action devuelve estados esperados. Un fallo inesperado se registra y se lanza:
try {
return await updateProfile(input);
} catch (error) {
if (error instanceof ConcurrentUpdateError) {
return { status: "conflict", message: "El perfil cambió en otra sesión." };
}
reportServerError(error);
throw error;
}No devuelvas error.message de DB al cliente.
HTTP traduce resultados a status:
invalid input → 400/422
no session → 401
no permission → 403 or 404
conflict → 409/412
rate limit → 429
unexpected → 500Usa un formato consistente y request ID. No respondas 200 con un campo error.
pending resource
→ Suspense fallback
rejected resource / render error
→ Error BoundaryCombina ambas:
<ErrorBoundary fallback={<RecommendationsError />}>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</ErrorBoundary>Un timeout puede convertirse en error y activar la boundary. Suspense no es manejo de fallos.
Cada dependencia necesita límite:
const response = await fetch(url, {
signal: AbortSignal.timeout(4_000),
});Para DB/SDK usa configuración propia. Define timeout menor al límite de plataforma para conservar tiempo de responder y registrar.
No todos los requests merecen el mismo límite: auth, search y export tienen perfiles distintos.
Reintenta solo cuando:
No reintentes:
retry 1 after 100ms + jitter
retry 2 after 300ms + jitter
then degrade/failCuando un proveedor falla repetidamente:
closed → requests pass
failures exceed threshold
→ open → fail fast/use fallback
cooldown
→ half-open → limited probesEn serverless distribuido, el estado del breaker necesita una store compartida o una librería/plataforma; una variable global solo protege una instancia.
Aísla recursos:
Un servicio de recomendaciones lento no debería agotar conexiones de checkout.
Ejemplos:
La fallback depende de criticidad. “Mostrar datos viejos” puede ser aceptable para contenido, pero peligroso para permisos o stock.
SWR puede servir una versión stale cuando el proveedor está lento. Define:
No cachees errores transitorios durante horas salvo intención.
Si el shell ya fue enviado, un error posterior no puede cambiar fácilmente el HTTP status. La boundary puede mostrar fallo dentro de una respuesta 200.
Para SEO/not found crítico, resuelve la entidad antes de iniciar stream. Para widgets secundarios, el aislamiento es más importante que el status.
Un mismatch puede hacer que React regenere parte del árbol cliente o deje event handlers inesperados. Trátalo como bug:
Usa onRecoverableError de hydrateRoot a través de las capacidades del framework/instrumentation cuando sea posible. No silencies ampliamente.
Después de deploy:
old tab → old chunk URL → 404Una boundary puede ofrecer “Recargar aplicación”. Conserva assets antiguos/CDN durante una ventana. No recargues automáticamente en loop.
El navegador puede perder red durante una Action. Preserva input y comunica que no se confirmó.
Para offline real necesitas:
No presentes un optimistic state como guardado si no existe confirmación.
Registra en servidor:
No registres tokens, passwords, FormData completa ni PII innecesaria.
En cliente, captura boundary, route y release. Los errores cliente pueden ser causados por extensiones; añade contexto sin asumir culpabilidad de la app.
Puede inicializar OpenTelemetry/SDK servidor y exportar hooks como onRequestError bajo la API vigente. Úsalo para observabilidad central en vez de repetir console.error.
Distingue Node y Edge instrumentation si tu despliegue utiliza ambos.
Producción puede ocultar detalles y entregar un digest. Úsalo para correlacionar el mensaje del usuario con logs, no como información de negocio.
Un error útil responde:
Evita “Something went wrong” sin contexto o stack técnico.
No culpes al usuario por un fallo servidor.
invalid form
→ field errors
stock conflict
→ 409/state conflict, preserve cart
payment timeout
→ check idempotency/payment status before retry
recommendations fail
→ hide widget
DB outage
→ error boundary + incident loggingCada fallo tiene una recuperación distinta.
Chaos testing selectivo en staging ayuda a validar degradación.
Oculta fallos y crea UI incoherente.
Duplica efectos.
Miente y puede ser indexado.
Toda la app cae por widget.
Fragmenta UX y código.
Loop de error.
Incidente adicional.
Permisos/stock incorrectos.
reset()?error.tsx del mismo segmento?Accesibilidad en aplicaciones Next.js aplica semántica, foco y anuncios a navegación, streaming, formularios y rutas dinámicas.