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:

  1. Usuario navega a /productos
  2. page.tsx intenta cargar datos
  3. Ocurre un error (API caída, timeout, etc.)
  4. NextJS captura el error automáticamente
  5. Muestra error.tsx al usuario
  6. 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:

PropTipoDescripción
errorErrorEl error que se lanzó
error.messagestringMensaje del error
error.digeststring?Hash único del error (útil para logging)
resetfunctionFunció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 existe global-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:

  1. Limpia el error boundary
  2. Re-renderiza el contenido de la página
  3. 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:

Aspectoerror.tsxnot-found.tsx
Cuándo usarloErrores del servidor, fallosRecurso no encontrado
Status HTTP500404
Se disparaAutomáticamente en errorLlamando notFound()
Tiene resetNo (no hay nada que recuperar)
Client ComponentObligatorioPuede 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:

ArchivoAlcanceClient ComponentTiene resetStatus HTTP
error.tsxPágina y componentes anidados✓ Obligatorio✓ Sí500
global-error.tsxLayout raíz✓ Obligatorio✓ Sí500
not-found.tsxRecursos no encontrados✗ Puede ser Server✗ No404

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:

  1. error.tsx captura errores en páginas y componentes anidados
  2. DEBE ser Client Component ('use client')
  3. Recibe error (info del error) y reset() (reintentar)
  4. No captura errores en layout.tsx del mismo nivel
  5. global-error.tsx captura errores en el layout raíz
  6. not-found.tsx maneja recursos no encontrados (404)
  7. Usa notFound() para disparar 404 programáticamente
  8. Logging de errores es esencial para debugging
  9. Mensajes de error deben ser amigables y accionables
  10. Prevenir errores es mejor que solo manejarlos

Tabla de decisión:

SituaciónUsar
Recurso no existenotFound() + not-found.tsx
Error en fetch/databaseerror.tsx con reset()
Error en layouterror.tsx en nivel superior
Error crítico globalglobal-error.tsx
Validación de paramsnotFound() si inválido