Manejo de Errores
Receta
Maneja errores de forma elegante en Next.js 15+ App Router usando error.tsx límites, global-error.tsx para fallos a nivel raíz, not-found.tsx para 404s, y almacenamiento de errores estructurado.
Ejemplo de Trabajo
Error Boundary a Nivel de Ruta
// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Registra en tu servicio de reporte de errores
console.error("Error del dashboard:", error);
}, [error]);
return (
<div role="alert">
<h2>Algo salió mal</h2>
<p>{error.message}</p>
{error.digest && (
<p className="text-sm text-gray-500">ID del Error: {error.digest}</p>
)}
<button onClick={reset}>Intentar de nuevo</button>
</div>
);
}Manejador de Error Global
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div role="alert">
<h1>Error de la Aplicación</h1>
<p>Ocurrió un error inesperado.</p>
<button onClick={reset}>Recargar</button>
</div>
</body>
</html>
);
}Página No Encontrada
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h1>404 - Página No Encontrada</h1>
<p>La página que buscas no existe.</p>
<Link href="/">Ir al inicio</Link>
</div>
);
}Not Found Programático en Server Components
// app/posts/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) {
notFound(); // Renderiza el not-found.tsx más cercano
}
return <article>{post.content}</article>;
}Utilidad de Almacenamiento de Errores
// lib/logger.ts
type ErrorContext = {
userId?: string;
path?: string;
action?: string;
metadata?: Record<string, unknown>;
};
export function logError(error: unknown, context?: ErrorContext) {
const errorObj = error instanceof Error ? error : new Error(String(error));
const payload = {
message: errorObj.message,
stack: errorObj.stack,
timestamp: new Date().toISOString(),
...context,
};
// Reemplaza con Sentry, Axiom, o tu servicio preferido
if (process.env.NODE_ENV === "production") {
fetch("/api/log", {
method: "POST",
body: JSON.stringify(payload),
}).catch(() => {
// Suprime errores de almacenamiento para evitar fallos en cascada
});
} else {
console.error("[Error]", payload);
}
}Manejo de Errores en Server Actions
// app/actions.ts
"use server";
import { logError } from "@/lib/logger";
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function createPost(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
try {
const title = formData.get("title") as string;
if (!title) {
return { success: false, error: "El título es obligatorio" };
}
const post = await db.post.create({ data: { title } });
return { success: true, data: { id: post.id } };
} catch (error) {
logError(error, { action: "createPost" });
return { success: false, error: "No se pudo crear la publicación" };
}
}Análisis Profundo
Cómo Funciona
error.tsxes un Client Component que envuelve los hijos del segmento de ruta en un React Error Boundary. Detecta errores lanzados durante el renderizado, en Server Components, y durante la obtención de datos.global-error.tsxdetecta errores en el layout raíz. Debe renderizar sus propias etiquetas<html>y<body>porque reemplaza el layout raíz completo cuando se activa.not-found.tsxse renderiza cuando se llama anotFound()o cuando ninguna ruta coincide. Se usa elnot-found.tsxmás cercano en el árbol de componentes.- Los error boundaries no detectan errores en manejadores de eventos, limpieza de
useEffect, o código asincrónico en Client Components. Usa try/catch para esos casos. - La propiedad
digestes un hash generado por Next.js para errores del lado del servidor. Permite correlacionar errores visibles para el usuario con registros del servidor sin exponer trazas sensibles. - La función
resetintenta volver a renderizar los hijos del error boundary. Funciona para errores transitorios (problemas de red) pero no para bugs persistentes.
Variaciones
Error Boundary con Reintentos y Fallback:
"use client";
import { useEffect, useState } from "react";
export default function ErrorWithRetry({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
if (retryCount > 0) {
reset();
}
}, [retryCount, reset]);
if (retryCount >= 3) {
return (
<div>
<h2>Error Persistente</h2>
<p>Por favor contacta soporte. ID del Error: {error.digest}</p>
</div>
);
}
return (
<div role="alert">
<h2>Ocurrió un Error</h2>
<button onClick={() => setRetryCount((c) => c + 1)}>
Reintentar ({3 - retryCount} intentos restantes)
</button>
</div>
);
}Manejo de Errores en Route Handler:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { logError } from "@/lib/logger";
export async function GET(request: NextRequest) {
try {
const posts = await db.post.findMany();
return NextResponse.json(posts);
} catch (error) {
logError(error, { path: "/api/posts", action: "GET" });
return NextResponse.json(
{ error: "Error interno del servidor" },
{ status: 500 }
);
}
}Notas de TypeScript
- El tipo de prop
erroresError & { digest?: string }. Eldigestes opcional y solo está presente para errores del lado del servidor. - Server Actions debe devolver una unión discriminada (
{ success: true; data: T } | { success: false; error: string }) para manejo de errores con seguridad de tipos en el cliente. - Evita usar
throwen Server Actions para errores de validación. Reservathrowpara fallos inesperados que deberían activar el error boundary.
Trampas
error.tsxdebe ser un Client Component. Requiere la directiva"use client". Olvidar esto produce un error de compilación.error.tsxno detecta errores en ellayout.tsxdel mismo nivel. Para detectar errores de layout, colocaerror.tsxen el segmento padre, o usaglobal-error.tsxpara el layout raíz.global-error.tsxsolo se activa en producción. En desarrollo, se muestra la superposición de error de Next.js en su lugar.redirect()lanza un error especial. Si envuelvesredirect()en un try/catch dentro de un Server Component, la redirección será capturada y suprimida. O relanza errores deNEXT_REDIRECTo llama aredirect()fuera del try/catch.- Los detalles de error del lado del servidor están ocultos en producción. Next.js elimina trazas de pila y mensajes de errores mostrados al cliente. Usa el
digestpara correlacionar con registros del servidor.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
error.tsx boundary | Integrado, automático, por ruta | Solo Client Component, sin errores de layout |
global-error.tsx | Detecta errores de layout raíz | Debe renderizar su propio html/body, solo producción |
| Try/catch en Server Actions | Control granular, retorna errores tipados | Manual, sin boundary automático |
| Sentry o Datadog | Seguimiento rico de errores, alertas | Dependencia externa, costo |
React ErrorBoundary clase | Control total, reutilizable | Verboso, sin errores de Server Component |
Preguntas Frecuentes
¿Por qué debe error.tsx ser un Client Component?
- Los React Error Boundaries son un concepto del lado del cliente que se basan en métodos del ciclo de vida del componente de clase.
- La directiva
"use client"es necesaria para queerror.tsxfuncione como un error boundary. - Olvidar la directiva produce un error de compilación.
¿Qué es la propiedad digest en el objeto error?
- Es un hash generado por Next.js para errores del lado del servidor.
- Te permite correlacionar errores visibles para el usuario con registros del servidor sin exponer trazas de pila.
- Solo está presente en errores que originaron en el servidor.
¿Puede error.tsx detectar errores lanzados en el layout.tsx del mismo nivel?
- No.
error.tsxsolo detecta errores en los hijos del segmento de ruta. - Para detectar errores de layout, coloca
error.tsxen el segmento padre. - Para errores de layout raíz, usa
global-error.tsx.
¿Por qué global-error.tsx necesita sus propias etiquetas <html> y <body>?
- Cuando
global-error.tsxse activa, reemplaza el layout raíz completo. - Sin sus propias etiquetas
<html>y<body>, la página no tendría estructura de documento. - Esto solo se activa en producción; el desarrollo muestra la superposición de error de Next.js.
Trampa: ¿Qué sucede si envuelves redirect() en un try/catch dentro de un Server Component?
redirect()lanza un error especialNEXT_REDIRECT.- Un try/catch capturará y suprimirá este error, evitando la redirección.
- O relanza errores de
NEXT_REDIRECTo llama aredirect()fuera del bloque try/catch.
¿Cuándo deberías usar throw vs. devolver un objeto error en una Server Action?
- Usa
throwpara fallos inesperados que deberían activar el error boundary más cercano. - Devuelve un objeto error tipado (
{ success: false; error: string }) para errores de validación esperados. - Esto proporciona al cliente control granular sobre cómo mostrar mensajes de validación.
¿Qué hace la función reset, y cuándo falla?
reset()vuelve a renderizar los hijos del error boundary, intentando la recuperación.- Funciona para errores transitorios como tiempos de espera de red.
- No corrige bugs persistentes; el mismo error será lanzado nuevamente al volver a renderizar.
¿Cómo escribiría una Server Action result como una unión discriminada en TypeScript?
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function createPost(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
// ...
}- El discriminante
successpermite a TypeScript estrechar el tipo al verificar el resultado.
¿Cómo escribes el tipo de la prop error en error.tsx con TypeScript?
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// ...
}Error & { digest?: string }es el tipo requerido. Eldigestes opcional.
¿Por qué la utilidad de almacenamiento de errores suprime sus propios errores de fetch?
- Si el servicio de almacenamiento en sí está caído, un error lanzado sería en cascada y ocultaría el error original.
- Usar
.catch(() => {})asegura que los fallos de almacenamiento sean silenciosos. - Esto evita que un logger roto derrumbe toda la aplicación.
Trampa: ¿Por qué los detalles de error del lado del servidor están ocultos del cliente en producción?
- Next.js elimina trazas de pila y mensajes de error para evitar filtrar detalles de implementación sensibles.
- Solo se expone el hash
digest, que puedes usar para encontrar el error completo en registros del servidor. - En desarrollo, se muestra la superposición de error completa en su lugar.
¿Qué errores NO detecta error.tsx?
- Errores en manejadores de eventos y funciones de limpieza de
useEffect. - Errores en código asincrónico dentro de Client Components (a menos que se capturen manualmente).
- Errores en el
layout.tsxdel mismo nivel. - Usa try/catch para manejadores de eventos y
global-error.tsxpara fallos de layout raíz.
Relacionado
- Authentication - manejo de fallos de autenticación
- API Route Handlers - respuestas de error en Route Handlers
- Testing - pruebas de estados de error
- Documentación de Manejo de Errores de Next.js