Error Handling
Los errores son inevitables. NextJS 15 proporciona archivos especiales para manejarlos de forma elegante: error.tsx
para errores generales y not-found.tsx
para recursos no encontrados (404).
¿Qué es error.tsx?
error.tsx
es un archivo especial que captura errores que ocurren en page.tsx
y componentes anidados. Cuando algo falla, NextJS muestra este archivo en lugar de romper toda la aplicación.
app/
└── productos/
├── page.tsx ← Si falla aquí
└── error.tsx ← Se muestra esto
Flujo cuando hay un error:
- Usuario navega a
/productos
page.tsx
intenta cargar datos- Ocurre un error (API caída, timeout, etc.)
- NextJS captura el error automáticamente
- Muestra
error.tsx
al usuario - La aplicación sigue funcionando (no se rompe todo)
Basado en React Error Boundaries
Internamente, NextJS usa React Error Boundaries. error.tsx
envuelve automáticamente tu página en un boundary que captura errores.
Sintaxis básica
Error simple
// app/productos/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Algo salió mal</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Intentar de nuevo
</button>
</div>
)
}
Client Component obligatorio
error.tsx
DEBE ser un Client Component (usar 'use client'
). Esto es porque necesita usar React hooks y event handlers para la funcionalidad de recuperación.
Props que recibe:
Prop | Tipo | Descripción |
---|---|---|
error | Error | El error que se lanzó |
error.message | string | Mensaje del error |
error.digest | string? | Hash único del error (útil para logging) |
reset | function | Función para reintentar renderizar la página |
Error con diseño personalizado
// app/productos/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-md mx-auto text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h1 className="text-3xl font-bold mb-2">Oops!</h1>
<h2 className="text-xl text-gray-700 mb-4">
No pudimos cargar los productos
</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<div className="space-y-3">
<button
onClick={reset}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Intentar de nuevo
</button>
<a
href="/"
className="block w-full px-4 py-2 border border-gray-300 rounded hover:bg-gray-50"
>
Volver al inicio
</a>
</div>
</div>
</div>
)
}
Alcance de error.tsx
Un archivo error.tsx
captura errores en su ruta y todas las rutas anidadas debajo:
app/
└── productos/
├── error.tsx ← Captura errores en /productos y subrutas
├── page.tsx ✓ Protegido por este error.tsx
└── [id]/
└── page.tsx ✓ También protegido por este error.tsx
Qué captura:
- Errores en
page.tsx
- Errores en componentes que renderiza
page.tsx
- Errores al obtener datos (fetch, database)
- Errores de runtime (acceder a propiedades undefined, etc.)
Qué NO captura:
- Errores en
layout.tsx
del mismo nivel - Errores en
error.tsx
mismo (para eso existeglobal-error.tsx
)
Error específico para rutas anidadas
Puedes tener diferentes error handlers en diferentes niveles:
app/
└── productos/
├── error.tsx ← Para /productos
├── page.tsx
└── [id]/
├── error.tsx ← Para /productos/[id] (más específico)
└── page.tsx
El error más cercano a donde ocurrió el error es el que se muestra.
Granularidad de errores
Crea error.tsx
específicos en rutas críticas para mensajes personalizados. Por ejemplo, un error al cargar un producto puede ser más específico que un error general.
La función reset()
reset()
permite al usuario reintentar la operación que falló:
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Intentar de nuevo</button>
</div>
)
}
Qué hace reset:
- Limpia el error boundary
- Re-renderiza el contenido de la página
- Vuelve a ejecutar el data fetching
Ejemplo de uso efectivo:
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('Error capturado:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center p-8">
<h2 className="text-2xl font-bold mb-4">Algo salió mal</h2>
{process.env.NODE_ENV === 'development' && (
<pre className="bg-gray-100 p-4 rounded mb-4 text-sm overflow-auto">
{error.message}
</pre>
)}
<button
onClick={() => reset()}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Reintentar
</button>
</div>
)
}
Layouts y Errores
Los errores en layout.tsx
NO son capturados por error.tsx
del mismo nivel:
app/
└── productos/
├── layout.tsx ← Error aquí NO es capturado por error.tsx
├── error.tsx ← Solo captura errores de page.tsx
└── page.tsx ← Error aquí SÍ es capturado
Razón: El error.tsx se renderiza dentro del layout, entonces no puede capturar errores del layout mismo.
Solución: Poner error.tsx
en el nivel superior:
app/
├── error.tsx ← Captura errores del layout de productos
└── productos/
├── layout.tsx ✓ Ahora este error SÍ es capturado
├── error.tsx ← Captura errores de page.tsx
└── page.tsx
Jerarquía visual
layout.tsx del nivel superior
└─ error.tsx del nivel superior (captura errores del layout hijo)
└─ layout.tsx de productos
└─ error.tsx de productos (captura errores de page.tsx)
└─ page.tsx
Global Error Handler
Para capturar errores en el layout.tsx
raíz (app/layout.tsx), usa global-error.tsx
:
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Algo salió muy mal</h2>
<p className="mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Intentar de nuevo
</button>
</div>
</body>
</html>
)
}
Incluir tags html y body
global-error.tsx
reemplaza el layout raíz completo cuando hay error, por lo que DEBES incluir las tags html y body.
Cuándo usar:
- Para errores muy raros que rompen el layout principal
- Como red de seguridad final
- En producción, para evitar pantallas blancas
not-found.tsx
Para manejar recursos no encontrados (404), usa not-found.tsx
:
app/
└── productos/
├── [id]/
│ ├── page.tsx
│ └── not-found.tsx ← Página 404 personalizada
└── not-found.tsx ← 404 para /productos
Sintaxis básica
// app/productos/[id]/not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold mb-2">404</h1>
<h2 className="text-2xl mb-4">Producto no encontrado</h2>
<p className="text-gray-600 mb-6">
El producto que buscas no existe o fue removido
</p>
<a
href="/productos"
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Ver todos los productos
</a>
</div>
)
}
Disparar not-found programáticamente
Usa la función notFound()
de Next.js:
// app/productos/[id]/page.tsx
import { notFound } from 'next/navigation'
export default async function ProductoPage({
params,
}: {
params: { id: string }
}) {
const producto = await obtenerProducto(params.id)
if (!producto) {
notFound()
}
return (
<div>
<h1>{producto.nombre}</h1>
<p>{producto.descripcion}</p>
</div>
)
}
Diferencia con error.tsx:
Aspecto | error.tsx | not-found.tsx |
---|---|---|
Cuándo usarlo | Errores del servidor, fallos | Recurso no encontrado |
Status HTTP | 500 | 404 |
Se dispara | Automáticamente en error | Llamando notFound() |
Tiene reset | Sí | No (no hay nada que recuperar) |
Client Component | Obligatorio | Puede ser Server Component |
not-found.tsx global
// app/not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-6xl font-bold text-gray-800 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-700 mb-4">
Página no encontrada
</h2>
<p className="text-gray-600 mb-8 text-center max-w-md">
La página que buscas no existe. Puede que haya sido movida o eliminada.
</p>
<div className="space-x-4">
<a
href="/"
className="px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Volver al inicio
</a>
<a
href="/productos"
className="px-6 py-3 border border-gray-300 rounded hover:bg-gray-50"
>
Ver productos
</a>
</div>
</div>
)
}
Ejemplos prácticos con e-commerce
1. Error al cargar lista de productos
// app/productos/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-lg mx-auto text-center">
<div className="mb-8">
<svg
className="w-24 h-24 mx-auto text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h1 className="text-3xl font-bold mb-4">
No pudimos cargar los productos
</h1>
<p className="text-gray-600 mb-8">
Estamos teniendo problemas para conectar con nuestros servidores.
Por favor intenta de nuevo en unos momentos.
</p>
{process.env.NODE_ENV === 'development' && (
<div className="bg-red-50 border border-red-200 rounded p-4 mb-6 text-left">
<p className="text-sm font-mono text-red-800">{error.message}</p>
{error.digest && (
<p className="text-xs text-red-600 mt-2">
Error ID: {error.digest}
</p>
)}
</div>
)}
<div className="space-y-3">
<button
onClick={reset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Intentar de nuevo
</button>
<a
href="/"
className="block w-full px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Volver al inicio
</a>
</div>
</div>
</div>
)
}
2. Producto no encontrado
// app/productos/[id]/not-found.tsx
export default function NotFound() {
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-lg mx-auto text-center">
<div className="mb-8">
<svg
className="w-24 h-24 mx-auto text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<h1 className="text-4xl font-bold mb-4">Producto no encontrado</h1>
<p className="text-gray-600 mb-8">
El producto que buscas no existe, fue removido, o el enlace es incorrecto.
</p>
<div className="space-y-3">
<a
href="/productos"
className="block w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Ver todos los productos
</a>
<a
href="/productos/destacados"
className="block w-full px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Ver productos destacados
</a>
</div>
</div>
</div>
)
}
3. Error al procesar pago
// app/checkout/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('Error en checkout:', error)
}, [error])
const esErrorPago = error.message.includes('payment')
const esErrorTimeout = error.message.includes('timeout')
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-lg mx-auto">
<div className="bg-red-50 border-l-4 border-red-500 p-6 rounded-r mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="w-6 h-6 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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>
</div>
<div className="ml-3">
<h3 className="text-lg font-medium text-red-800">
{esErrorPago && 'Error al procesar el pago'}
{esErrorTimeout && 'Se agotó el tiempo de espera'}
{!esErrorPago && !esErrorTimeout && 'Error en el checkout'}
</h3>
<p className="mt-2 text-sm text-red-700">
{esErrorPago && 'Tu pago no pudo ser procesado. No se realizó ningún cargo.'}
{esErrorTimeout && 'La operación tardó demasiado. Por favor intenta de nuevo.'}
{!esErrorPago && !esErrorTimeout && 'Ocurrió un error inesperado.'}
</p>
</div>
</div>
</div>
<div className="space-y-3">
<button
onClick={reset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Intentar de nuevo
</button>
<a
href="/carrito"
className="block w-full px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 text-center"
>
Volver al carrito
</a>
<a
href="/ayuda"
className="block text-center text-blue-600 hover:underline"
>
Contactar soporte
</a>
</div>
<div className="mt-8 p-4 bg-blue-50 rounded">
<h4 className="font-medium mb-2">¿Necesitas ayuda?</h4>
<p className="text-sm text-gray-600">
Si el problema persiste, contacta a nuestro equipo de soporte.
Incluye este código de error: <code>{error.digest || 'N/A'}</code>
</p>
</div>
</div>
</div>
)
}
Logging de errores
Es importante registrar los errores para debugging:
// app/productos/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
logError({
message: error.message,
digest: error.digest,
stack: error.stack,
timestamp: new Date().toISOString(),
url: window.location.href,
})
}, [error])
return <div>Error UI</div>
}
async function logError(errorInfo: {
message: string
digest?: string
stack?: string
timestamp: string
url: string
}) {
if (process.env.NODE_ENV === 'production') {
await fetch('/api/errors', {
method: 'POST',
body: JSON.stringify(errorInfo),
})
} else {
console.error('Error capturado:', errorInfo)
}
}
Diferencias entre archivos de error
Resumen de los diferentes archivos:
Archivo | Alcance | Client Component | Tiene reset | Status HTTP |
---|---|---|---|---|
error.tsx | Página y componentes anidados | ✓ Obligatorio | ✓ Sí | 500 |
global-error.tsx | Layout raíz | ✓ Obligatorio | ✓ Sí | 500 |
not-found.tsx | Recursos no encontrados | ✗ Puede ser Server | ✗ No | 404 |
Errores comunes y soluciones
1. Olvidar 'use client' en error.tsx
// ❌ Incorrecto - falla en runtime
export default function Error({ error, reset }) {
return <div>Error</div>
}
// ✓ Correcto
'use client'
export default function Error({ error, reset }) {
return <div>Error</div>
}
2. No validar antes de usar notFound()
// ❌ Puede causar problemas
export default async function Page({ params }) {
const data = await obtenerDatos(params.id)
notFound()
return <div>{data}</div>
}
// ✓ Correcto - validar antes
export default async function Page({ params }) {
const data = await obtenerDatos(params.id)
if (!data) {
notFound()
}
return <div>{data}</div>
}
3. No incluir html/body en global-error.tsx
// ❌ Incorrecto
export default function GlobalError({ error, reset }) {
return <div>Error global</div>
}
// ✓ Correcto
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<div>Error global</div>
</body>
</html>
)
}
Mejores prácticas
1. Mensajes de error amigables
// ✓ Bueno - mensaje claro y accionable
export default function Error({ error, reset }) {
return (
<div>
<h2>No pudimos cargar los productos</h2>
<p>Estamos teniendo problemas temporales.</p>
<button onClick={reset}>Intentar de nuevo</button>
<a href="/">Volver al inicio</a>
</div>
)
}
2. Múltiples opciones de recuperación
export default function Error({ error, reset }) {
return (
<div>
<h2>Algo salió mal</h2>
<button onClick={reset}>Reintentar</button>
<a href="/">Volver al inicio</a>
<a href="/ayuda">Contactar soporte</a>
<p className="text-sm text-gray-500">
Error ID: {error.digest}
</p>
</div>
)
}
3. Componente reutilizable
// components/ErrorUI.tsx
interface ErrorUIProps {
title: string
description: string
onReset?: () => void
showHomeLink?: boolean
errorId?: string
}
export function ErrorUI({
title,
description,
onReset,
showHomeLink = true,
errorId,
}: ErrorUIProps) {
return (
<div className="container mx-auto px-4 py-16">
<div className="max-w-lg mx-auto text-center">
<h1 className="text-3xl font-bold mb-4">{title}</h1>
<p className="text-gray-600 mb-8">{description}</p>
<div className="space-y-3">
{onReset && (
<button
onClick={onReset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Intentar de nuevo
</button>
)}
{showHomeLink && (
<a
href="/"
className="block w-full px-6 py-3 border border-gray-300 rounded-lg"
>
Volver al inicio
</a>
)}
</div>
{errorId && (
<p className="text-sm text-gray-500 mt-6">
Error ID: {errorId}
</p>
)}
</div>
</div>
)
}
Resumen
Puntos clave sobre Error Handling:
error.tsx
captura errores en páginas y componentes anidados- DEBE ser Client Component (
'use client'
) - Recibe
error
(info del error) yreset()
(reintentar) - No captura errores en
layout.tsx
del mismo nivel global-error.tsx
captura errores en el layout raíznot-found.tsx
maneja recursos no encontrados (404)- Usa
notFound()
para disparar 404 programáticamente - Logging de errores es esencial para debugging
- Mensajes de error deben ser amigables y accionables
- Prevenir errores es mejor que solo manejarlos
Tabla de decisión:
Situación | Usar |
---|---|
Recurso no existe | notFound() + not-found.tsx |
Error en fetch/database | error.tsx con reset() |
Error en layout | error.tsx en nivel superior |
Error crítico global | global-error.tsx |
Validación de params | notFound() si inválido |