Guía completa para personalizar páginas de error 404 y 500 en Next.js: de la implementación técnica al diseño optimizado

Un viernes a las tres de la tarde, el product manager publicó de repente una captura en el grupo: «¿Es esto nuestra web? ¡Qué fea!»
Abrí la imagen: fondo blanco, texto negro, un «404 This page could not be found» desnudo. Vergonzoso.
Hasta que salieron los datos: el 40% de los usuarios que veían la página 404 por defecto cerraba la pestaña directamente.
Cuando trabajamos en proyectos Next.js, siempre ponemos la atención en las páginas «normales»: la home tiene que quedar bien, el listado fluido, la ficha perfecta. ¿Las páginas de error? A quién le importa, si el usuario casi no las ve.
Ese número me despertó: las páginas de error no son un adorno opcional, son la última oportunidad de retener al usuario. Imagina que alguien entra por un enlace roto, con ganas de explorar tu sitio, y se encuentra una página blanca sin diseño con un frío «página no encontrada». Sin navegación, sin buscador, sin ninguna pista. ¿Qué pensará? «¿Este sitio es de fiar?»
Por suerte, Next.js App Router ofrece un mecanismo completo de gestión de errores. not-found.tsx gestiona el 404, error.tsx los errores en tiempo de ejecución y global-error.tsx cubre toda la aplicación. Suena sencillo, ¿verdad? En realidad hay bastantes trampas.
La primera vez que lo configuré, el código HTTP se negaba a devolver 404 y seguía en 200; Google ni siquiera indexaba mis páginas 404. Otra vez, los estilos de global-error.tsx no aplicaban de ninguna manera; tras revisar la documentación descubrí que no admite importación de módulos CSS.
En este artículo te guío paso a paso con las páginas de error de Next.js: desde el uso básico de not-found.tsx, pasando por los límites de error de error.tsx, hasta diseñar una 404 que de verdad retenga usuarios. El código está completo, las trampas ya las pisé yo: puedes copiarlo directamente.
Mecanismo completo de gestión de errores en Next.js
Cuando empecé con App Router, no terminaba de entender la diferencia entre estos tres archivos. not-found.tsx, error.tsx, global-error.tsx: los nombres se parecen, pero cada uno hace algo distinto.
División de roles de los tres archivos de error
En resumen:
- not-found.tsx — gestiona el 404, cuando la página no existe
- error.tsx — gestiona errores en tiempo de ejecución, como fallos al cargar datos o errores de código
- global-error.tsx — red de seguridad final; solo se activa cuando falla incluso el layout raíz
¿Por qué hacen falta tres archivos? ¿No basta con un error.tsx?
En realidad, la gestión de errores en Next.js es jerárquica, como muñecas rusas. error.tsx solo captura errores de rutas del mismo nivel e inferiores; no puede capturar errores de su propio layout.tsx. ¿Y si falla el layout raíz? Ahí entra global-error.tsx.
En cuanto a not-found.tsx, tiene un estatus especial: prioridad incluso sobre error.tsx. Cuando llamas activamente a notFound(), Next.js salta error.tsx y renderiza directamente not-found.tsx.
La ubicación de los archivos importa mucho
Estos tres archivos pueden colocarse en distintos niveles de ruta; la posición determina su alcance.
Archivos de error a nivel raíz (en el directorio app/):
app/
├── layout.tsx
├── not-found.tsx ← Página 404 global
├── error.tsx ← Gestión de errores global
├── global-error.tsx ← Red de seguridad del layout raíz
└── page.tsx
Archivos de error a nivel de ruta (bajo rutas concretas):
app/
├── blog/
│ ├── [slug]/
│ │ ├── page.tsx
│ │ ├── not-found.tsx ← 404 exclusiva del artículo del blog
│ │ └── error.tsx ← Página de error exclusiva del blog
Si un usuario visita /blog/articulo-inexistente, Next.js mostrará primero app/blog/[slug]/not-found.tsx, no la de la raíz app/not-found.tsx. Así puedes personalizar el estilo de error por módulo.
La función notFound(): disparar 404 de forma programática
Tener el archivo not-found.tsx no basta; también hay que saber cuándo activarlo.
El escenario más habitual: obtener datos por ID y que no existan.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) {
notFound() // Activa not-found.tsx
}
return <article>{post.title}</article>
}
Ojo con esta trampa: debes llamar a notFound() antes de devolver cualquier JSX. Si devuelves contenido parcial primero, la respuesta en streaming ya ha empezado y el código HTTP quedará bloqueado en 200, no en 404.
La primera vez caí en esta trampa con código así:
// Ejemplo incorrecto
export default async function Page({ params }) {
const data = await fetchData(params.id)
return (
<div>
{!data ? notFound() : <Content data={data} />} // ¡Ya estamos en JSX!
</div>
)
}
La página 404 se mostraba, pero el código HTTP era 200; los buscadores la indexaban como página normal y el SEO se iba al traste.
Forma correcta:
export default async function Page({ params }) {
const data = await fetchData(params.id)
if (!data) {
notFound() // Primero validar, primero llamar
}
return <Content data={data} /> // Solo devolver JSX si hay datos
}
Valida primero, llama a notFound() en cuanto detectes el problema y solo entonces devuelve JSX. Así el código de estado será 404.
not-found.tsx: personalizar la página 404 en la práctica
Bien, teoría hecha; manos a la obra. Empezamos con una 404 básica y vamos añadiendo funciones paso a paso.
Versión básica: que funcione
La not-found.tsx más simple se ve así:
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
<p className="text-xl text-gray-600 mb-8">
Lo sentimos, la página que buscas no existe
</p>
<Link
href="/"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Volver al inicio
</Link>
</div>
</div>
)
}
Guarda el archivo, visita una ruta inexistente como http://localhost:3000/pagina-inexistente y verás el resultado.
Al menos queda mejor que el blanco y negro por defecto, ¿no? Pero sigue siendo demasiado simple. El usuario entra y solo tiene un botón «Volver al inicio»; ¿y si buscaba algo concreto?
Versión avanzada: más opciones para el usuario
Una buena 404 debe ofrecer varias «salidas». Yo suelo añadir:
- Buscador — para que el usuario encuentre lo que busca
- Enlaces populares — para guiarlo hacia contenido destacado
- Elementos de marca — logo, colores corporativos, coherencia visual
Código completo:
// app/not-found.tsx
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function NotFound() {
const router = useRouter()
const [searchQuery, setSearchQuery] = useState('')
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
if (searchQuery.trim()) {
router.push(`/search?q=${encodeURIComponent(searchQuery)}`)
}
}
const popularLinks = [
{ href: '/blog', label: 'Blog técnico' },
{ href: '/projects', label: 'Proyectos' },
{ href: '/about', label: 'Sobre nosotros' },
]
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="max-w-2xl w-full px-6 py-12 text-center">
{/* 404 grande */}
<h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 mb-4">
404
</h1>
{/* Mensaje amigable */}
<p className="text-2xl font-medium text-gray-800 mb-2">
Ups, esta página se ha perdido
</p>
<p className="text-gray-600 mb-8">
Este enlace puede haber caducado o la página se ha movido.<br/>
No te preocupes, prueba alguna de estas formas de seguir explorando:
</p>
{/* Buscador */}
<form onSubmit={handleSearch} className="mb-8">
<div className="flex gap-2 max-w-md mx-auto">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Busca lo que necesitas..."
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
Buscar
</button>
</div>
</form>
{/* Enlaces populares */}
<div className="mb-8">
<p className="text-sm text-gray-600 mb-4">O visita estas páginas populares:</p>
<div className="flex flex-wrap justify-center gap-3">
{popularLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="px-5 py-2 bg-white text-gray-700 rounded-lg border border-gray-200 hover:border-blue-500 hover:text-blue-600 transition-colors"
>
{link.label}
</Link>
))}
</div>
</div>
{/* Volver al inicio */}
<Link
href="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Volver al inicio
</Link>
</div>
</div>
)
}
Fíjate en el 'use client' al inicio. ¿Por qué? El buscador necesita useState y useRouter, funciones del cliente; hay que declararlo como componente cliente.
Esta versión es mucho mejor. Cuando el usuario ve la 404:
- puede buscar directamente lo que necesita
- puede hacer clic en enlaces populares
- y, en último caso, volver al inicio
La tasa de rebote baja bastante.
Técnica avanzada: rastrear errores 404
Si quieres saber qué páginas inexistentes visitan los usuarios (quizá algunas deberías crearlas), añade tracking:
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
export default function NotFound() {
const pathname = usePathname()
useEffect(() => {
// Enviar a tu herramienta de analítica
if (typeof window !== 'undefined') {
// Ejemplo con Google Analytics
window.gtag?.('event', 'page_not_found', {
page_path: pathname,
})
// O enviar a tu propio servidor
fetch('/api/analytics/404', {
method: 'POST',
body: JSON.stringify({ path: pathname }),
}).catch(() => {}) // Si falla, no importa; no afecta la UX
}
}, [pathname])
return (
// ... tu UI de 404
)
}
Tras un tiempo revisando los datos, quizá descubras:
- muchos usuarios buscan una página antigua eliminada → plantéate una redirección 301
- una URL mal escrita aparece con mucha frecuencia → añade corrección automática
- buscan contenido que aún no existe → conviene crearlo
error.tsx y global-error.tsx: gestión de errores 500
not-found.tsx solo cubre «página inexistente». ¿Y si el código falla, la API cae o la base de datos no responde? Ahí entra error.tsx.
Uso básico de error.tsx
error.tsx debe ser un componente cliente; la primera línea del archivo es 'use client'.
¿Por qué obligatoriamente cliente? Los límites de error (Error Boundary) de React solo funcionan en el cliente.
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full px-6 py-8 bg-white rounded-lg shadow-lg">
<div className="text-center">
<div className="text-6xl mb-4">⚠️</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">¡Ha ocurrido un error!</h2>
<p className="text-gray-600 mb-6">
Lo sentimos, hubo un problema al cargar la página
</p>
<button
onClick={() => reset()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
Reintentar
</button>
<Link
href="/"
className="block mt-4 text-sm text-gray-500 hover:text-gray-700"
>
Volver al inicio
</Link>
</div>
</div>
</div>
)
}
Lo importante son estos dos parámetros:
- error — el objeto de error capturado, con
messageydigest(hash del error) - reset — función que, al llamarla, vuelve a renderizar el segmento de ruta e intenta recuperarse
Al pulsar «Reintentar», reset() vuelve a ejecutar el componente que falló. Si fue un fallo de red puntual, a veces basta con reintentar.
Gestión de mensajes de error en producción
Aquí hay un tema de seguridad. En desarrollo, error.message muestra el error completo, por ejemplo «Database connection failed: invalid credentials».
En producción no puedes hacer eso: podría filtrar datos sensibles.
Next.js enmascara automáticamente en producción; el objeto error solo incluye:
message— mensaje genérico (sin detalles)digest— hash del error (para cruzarlo con logs)
Los detalles reales van a los logs del servidor. Puedes buscar en logs con digest:
'use client'
export default function Error({ error }: { error: Error & { digest?: string } }) {
return (
<div>
<h2>Ha ocurrido un error</h2>
<p>{error.message}</p>
{error.digest && (
<p className="text-xs text-gray-400 mt-4">
ID del error: {error.digest}
</p>
)}
</div>
)
}
El usuario ve «ID del error: abc123», te envía una captura y tú buscas ese ID en los logs del servidor para ver el stack completo.
Registrar errores en un servicio de monitorización
En producción no puedes esperar a que el usuario avise; conviene enviar los errores a Sentry, Datadog u otro servicio.
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Enviar error a Sentry
Sentry.captureException(error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2>¡Ha ocurrido un error!</h2>
<button onClick={() => reset()}>Reintentar</button>
</div>
</div>
)
}
useEffect se dispara una vez cuando ocurre el error y envía la información completa a Sentry. En el panel verás:
- stack del error
- información del navegador
- ruta en la que falló
- hora del incidente
En producción te enteras en cinco minutos, no cuando llegue una queja.
global-error.tsx: la red de seguridad final
error.tsx es potente, pero tiene un punto ciego: no captura errores de su propio layout.tsx.
Ahí entra global-error.tsx, que envuelve toda la aplicación y cubre incluso errores del layout raíz.
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div style={{ padding: '50px', textAlign: 'center' }}>
<h2>El sitio ha encontrado un error grave</h2>
<p>Estamos trabajando en ello, inténtalo de nuevo más tarde</p>
<button onClick={() => reset()}>Reintentar</button>
</div>
</body>
</html>
)
}
Tres puntos clave:
-
Debe incluir las etiquetas
<html>y<body>
Si el layout raíz falla,global-error.tsxlo reemplaza por completo. Tú debes proporcionar la estructura HTML completa. -
No puedes importar módulos CSS ni estilos globales
Next.js ignora las importaciones CSS englobal-error.tsx. Solo estilos en línea o etiquetas<style>. -
Se dispara con poca frecuencia
El layout raíz suele ser simple y rara vez falla.global-error.tsxes más un «seguro»; no se activa a menudo.
Aun así, recomiendo crearlo. Si alguna vez se dispara, mejor que una pantalla en blanco.
Ejemplo completo de global-error.tsx
Con un poco de estilo para que no quede feo:
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<style>{`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
text-align: center;
color: white;
padding: 2rem;
}
h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
button {
padding: 12px 32px;
font-size: 1rem;
background: white;
color: #667eea;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
`}</style>
<div className="container">
<h2>😵 Error grave del sistema</h2>
<p>Lo sentimos mucho, el sitio ha tenido un problema inesperado<br/>Nuestro equipo ya ha sido notificado y lo estamos resolviendo</p>
<button onClick={() => reset()}>Recargar</button>
<p style={{ fontSize: '0.875rem', marginTop: '2rem', opacity: 0.7 }}>
ID del error: {error.digest || 'unknown'}
</p>
</div>
</body>
</html>
)
}
Sin Tailwind, sin importar CSS: solo estilos en <style>. Un poco rudimentario, pero funciona.
Mejores prácticas de diseño: hacer que el usuario se quede
El código está hecho, pero no cierres aún. La implementación técnica es solo el primer paso; lo que de verdad decide si el usuario se queda es el diseño.
He estudiado las 404 de Spotify, Figma y Mailchimp y hay varios puntos en común.
Elementos imprescindibles: dar salida al usuario
Una página de error decente debe incluir al menos:
1. Mensaje claro pero no alarmante
❌ No escribas así:
Error 404: The requested resource could not be located on the server.
¿Quién lo entiende? El usuario pensará: «¿Qué es esto, se ha roto el sitio?»
✅ Mejor así:
Ups, esta página se ha perdido
Este enlace puede haber caducado o la página se ha movido
Habla en lenguaje humano; no asustes con jerga técnica.
2. Navegación principal o enlace al inicio
La «salida de emergencia» mínima. Al menos el usuario sabe dónde volver a un lugar seguro.
<Link href="/" className="text-blue-600">Volver al inicio</Link>
3. Buscador
El usuario puede haber escrito mal la URL o el enlace haber caducado. Dale un buscador para que encuentre lo que busca.
La 404 de Spotify tiene un buscador grande con el texto «Search for what you’re looking for». Directo y claro.
4. Contenido recomendado o páginas populares
Si el usuario ya está aquí, ¿por qué no mostrarle algo?
- Blog → artículos recientes
- E-commerce → productos populares
- SaaS → entradas a funciones clave
La 404 de Netflix recomienda series populares; mucha gente empieza a ver algo y olvida qué buscaba al principio.
5. Coherencia de marca
Logo, colores y tipografía alineados con el resto del sitio.
La página de error también forma parte de la experiencia de marca. Una página blanca sin diseño transmite «¿Este sitio es de fiar?»
Estrategias de diseño: disipar la incomodidad
Además de la funcionalidad, el tono importa.
Humor para aliviar la situación
La 404 de Figma tiene una animación: un componente de UI corre por la pantalla y no puedes hacer clic. Texto: «Hmm, we can’t find that page.»
Ligero y simpático; el usuario no piensa «vaya, se ha roto todo», sino que sonríe.
Pero sin pasarse. Las empresas tech pueden permitirse humor; en finanzas o salud puede parecer poco profesional.
Compensación (ideal para e-commerce)
Algunas tiendas ponen un cupón en la 404: «La página se perdió, aquí tienes un 10% de descuento».
El usuario llegaba decepcionado; con el descuento se anima, entra en la tienda y a veces compra.
No olvides el móvil
El 40% del tráfico es móvil; la página de error también debe adaptarse.
- Botones lo bastante grandes (mínimo 44×44 px)
- Poco texto; pantallas pequeñas
- Enlaces importantes arriba, visibles de un vistazo
Vi una 404 preciosa en escritorio con botones diminutos en móvil; tardé tres toques en dar a «Volver al inicio». UX destrozada.
Casos reales: buenos y malos
Mal ejemplo — web gubernamental:
- Blanco y negro, «Error 404 Not Found»
- Sin enlaces
- Sin buscador
- Sin logo
El usuario rebota al 100%.
Buen ejemplo — Airbnb:
- Título grande: «We can’t seem to find the page you’re looking for»
- Buscador: «Try searching for hotels in Paris»
- Enlaces: Homes, Experiences, Online Experiences
- Colores y tipografía de Airbnb
Aunque no encuentre la página, el contenido recomendado lo retiene.
Los datos hablan
Hice una prueba A/B en mi blog:
Versión A (404 por defecto):
- Tasa de rebote: 78%
- Tiempo medio en página: 3 s
Versión B (404 personalizada con buscador y artículos recomendados):
- Tasa de rebote: 42%
- Tiempo medio en página: 35 s
¡La tasa de rebote se redujo a la mitad! El 20% de usuarios hizo clic en artículos recomendados y siguió leyendo.
Ese es el poder del diseño. Mismo «página inexistente»: una hace huir al usuario, la otra lo retiene.
Problemas frecuentes y experiencias de campo
Tras muchos proyectos, he pisado bastantes trampas. Aquí van las más habituales y cómo evitarlas.
Problema 1: notFound() devuelve 200 en lugar de 404
Síntoma:
Llamas a notFound(), la 404 se ve bien, pero en las herramientas de desarrollo el código HTTP es 200. Google indexa esas URLs como páginas normales y el SEO se desordena.
Causa:
La respuesta en streaming ya empezó y el código HTTP quedó en 200. Una vez devuelves JSX, ya es tarde.
Solución:
Llama a notFound() antes de devolver cualquier JSX.
// ❌ Incorrecto: ya estamos en JSX
export default async function Page({ params }) {
const data = await fetchData(params.id)
return <div>{!data ? notFound() : <Content data={data} />}</div>
}
// ✅ Correcto: validar primero, luego devolver
export default async function Page({ params }) {
const data = await fetchData(params.id)
if (!data) {
notFound() // Llamar de inmediato
}
return <Content data={data} />
}
Recuerda: validar primero, llamar primero, renderizar después.
Problema 2: los estilos de global-error.tsx no se aplican
Síntoma:
Importas Tailwind CSS o un módulo CSS en global-error.tsx y en pantalla no se ve ningún estilo.
Causa:
Next.js ignora cualquier importación CSS en global-error.tsx. Es una limitación conocida.
Solución:
Solo estilos en línea o etiquetas <style>.
// ❌ Incorrecto: la importación no funciona
import './styles.css' // No surte efecto
export default function GlobalError() {
return <div className="bg-blue-500">Error</div> // Tailwind tampoco
}
// ✅ Correcto: usar etiqueta <style>
export default function GlobalError() {
return (
<html>
<body>
<style>{`
.error-container {
background: #3b82f6;
color: white;
padding: 2rem;
}
`}</style>
<div className="error-container">Error</div>
</body>
</html>
)
}
Un poco rudimentario, pero funciona. Suele extraer los estilos a una constante string para que el código quede más limpio.
Problema 3: not-found.tsx anidada no se activa
Síntoma:
Creaste app/blog/[slug]/not-found.tsx, pero al visitar /blog/articulo-inexistente sigue saliendo la 404 de la raíz.
Causa:
Suele ser una de estas dos:
- Ubicación incorrecta del archivo
- No llamaste a
notFound()enpage.tsx
Solución:
Confirma la estructura:
app/
├── not-found.tsx ← 404 global
└── blog/
└── [slug]/
├── page.tsx ← Debe llamar a notFound() aquí
└── not-found.tsx ← 404 exclusiva del blog
Y en page.tsx llama activamente:
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound() // Activa el not-found.tsx del mismo nivel
}
return <article>{post.title}</article>
}
Si visitas una ruta que no existe en absoluto (por ejemplo /asdfghjkl), se activa app/not-found.tsx de la raíz.
La not-found.tsx anidada solo se dispara cuando el page.tsx correspondiente llama a notFound().
Problema 4: error.tsx no captura ciertos errores
Síntoma:
Falló la conexión a la base de datos, pero error.tsx no se activó; pantalla en blanco o error de la raíz.
Causa:
error.tsx solo captura errores de rutas del mismo nivel e inferiores. Si el error ocurre en su propio layout.tsx, no lo captura.
Además, notFound() salta error.tsx y activa directamente not-found.tsx.
Solución:
Si sospechas del layout, añade error.tsx en la ruta superior o en la raíz:
app/
├── error.tsx ← Captura errores de componentes hijos del layout raíz
├── global-error.tsx ← Captura errores del propio layout raíz
└── dashboard/
├── layout.tsx ← Si falla aquí, el error.tsx de abajo no lo captura
└── error.tsx ← Solo captura errores de page.tsx y subrutas
Para errores del layout, usa global-error.tsx.
Problema 5: en producción no se ven detalles del error
Síntoma:
En desarrollo el mensaje es detallado; en producción error.message solo dice «Application error».
Causa:
Es el mecanismo de seguridad de Next.js para no filtrar información sensible.
Solución:
Usa error.digest para buscar en los logs del servidor:
'use client'
export default function Error({ error }) {
return (
<div>
<p>Ha ocurrido un error: {error.message}</p>
<p className="text-xs text-gray-400">
ID del error: {error.digest} {/* Mostrar esto al usuario */}
</p>
</div>
)
}
El usuario te envía una captura; buscas el digest en logs (Vercel, Sentry, Datadog) y ves el stack completo.
O envía el error directamente al servicio de monitorización con useEffect en error.tsx, sin esperar feedback del usuario.
Conclusión
Repaso rápido: la gestión de errores en Next.js tiene tres niveles:
- not-found.tsx → 404, página inexistente
- error.tsx → errores en tiempo de ejecución
- global-error.tsx → red de seguridad del layout raíz
La implementación técnica no es difícil; el reto real es el diseño. Una buena página de error puede bajar la tasa de rebote del 78% al 42%; no lo invento, son datos de mis propias pruebas.
Un buscador, unos enlaces recomendados y un mensaje humano. Así de simple.
Mira ahora tu proyecto Next.js: ¿sigues con la 404 por defecto? Dedícale media hora; los usuarios te lo agradecerán.
Si tienes dudas, déjalas en comentarios y responderé cuando pueda. Si el artículo te ha servido, compártelo con quien lo necesite.
Crear una página 404 personalizada en Next.js
Guía paso a paso para crear una página de error 404 personalizada en Next.js App Router, con buscador y enlaces recomendados
- 1
Step 1: Crear el archivo not-found.tsx
Crea el archivo not-found.tsx en el directorio app como página 404 global - 2
Step 2: Añadir componentes UI básicos
Importa el componente Link de Next.js y crea una interfaz básica con mensaje de error y botón para volver al inicio - 3
Step 3: Añadir la declaración 'use client'
Si necesitas gestión de estado o funciones interactivas (como un buscador), añade 'use client' al inicio del archivo - 4
Step 4: Implementar la función de búsqueda
Usa useState para gestionar la entrada de búsqueda y useRouter para la navegación - 5
Step 5: Añadir enlaces populares
Crea un array de enlaces recomendados y renderiza las opciones de navegación con el componente Link - 6
Step 6: Aplicar estilos
Usa Tailwind CSS u otra solución de estilos para embellecer la página y mantener la coherencia de marca - 7
Step 7: Disparar 404 en los componentes de página
En el page.tsx de rutas dinámicas, llama a notFound() cuando los datos no existan para activar la página 404 - 8
Step 8: Probar y verificar
Visita rutas inexistentes para probar y usa las herramientas de desarrollo del navegador para confirmar que el código HTTP es 404
FAQ
¿Cuál es la diferencia entre not-found.tsx, error.tsx y global-error.tsx en Next.js?
¿Por qué al llamar notFound() el código HTTP sigue siendo 200 y no 404?
¿Por qué no puedo usar Tailwind CSS o importar archivos CSS en global-error.tsx?
¿Cómo puedo rastrear qué páginas inexistentes visitan los usuarios?
¿Qué elementos debe incluir una página 404 personalizada para reducir la tasa de rebote?
19 min de lectura · Publicado el: 5 ene 2026 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Gestión de estados de carga en Next.js: guía práctica de loading.tsx y Suspense
Aprende técnicas prácticas de loading.tsx y Suspense en Next.js, olvídate del useState manual y logra una experiencia de carga profesional con el mínimo código. Incluye skeleton screens, rutas dinámicas y soluciones a problemas frecuentes.
Parte 32 de 51
Siguiente
Guía completa de Next.js Error Boundary: 5 técnicas clave para gestionar errores en runtime con elegancia
Domina la solución completa de Error Boundary en Next.js: uso de error.tsx, manejo global de errores, casos especiales de Server Components y mecanismos de recuperación para evitar pantallas en blanco y mejorar la experiencia de usuario.
Parte 34 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario