Gestionarea Erorilor în Next.js 16: Ghid Complet pentru error.tsx, not-found.tsx și global-error.tsx
Ghid complet pentru gestionarea erorilor în Next.js 16: error.tsx, not-found.tsx, global-error.tsx, Server Actions și integrare Sentry, cu exemple practice.
Gestionarea erorilor în Next.js 16 se face prin trei fișiere speciale plasate în app/: error.tsx prinde excepțiile dintr-un segment de rută și subordonatele lui, not-found.tsx răspunde la funcția notFound() sau la rute inexistente, iar global-error.tsx e plasa de siguranță pentru erorile care apar chiar în root layout. Toate trei sunt Client Components obligatoriu, primesc props error și reset (mai puțin not-found.tsx) și se integrează automat cu mecanismul de Error Boundary din React 19.
error.tsx e un Error Boundary client-side care prinde erorile dintr-un segment de rută; trebuie să fie marcat cu "use client".
global-error.tsx înlocuiește <html> și <body> și prinde erorile care scapă din root layout. E singurul loc unde acest lucru e permis.
not-found.tsx se declanșează automat când apelezi notFound() din next/navigation sau când utilizatorul accesează o rută inexistentă.
În Server Components, erorile aruncate sunt prinse de cel mai apropiat error.tsx; în Server Actions, returnează erori serializabile în loc să le arunci.
Funcția reset() re-randează segmentul, dar nu re-randează automat datele cache-uite. Adesea ai nevoie de router.refresh() sau revalidatePath().
În producție, mesajul real al erorii e înlocuit cu un text generic. Folosește error.digest pentru a corela cu logurile de pe server.
Cum funcționează gestionarea erorilor în App Router
Am venit la App Router din pages router, unde aveam _error.js și 404.js și gata. Un singur mecanism, un loc clar pentru tot. App Router împarte responsabilitatea pe trei fișiere și, pentru un timp, m-a încurcat că nu vedeam o ierarhie clară. Ierarhia există, doar că e definită de poziția fișierului în arborele app/, nu de un nume special de rută.
Mecanismul de bază. Next.js înfășoară fiecare segment de rută într-un React Error Boundary. Când un Server sau Client Component din acel segment aruncă o excepție în timpul randării, boundary-ul interceptează eroarea și randează componenta exportată din error.tsx. Dacă nu există error.tsx în segmentul curent, eroarea urcă în arbore către părinte, până ajunge la global-error.tsx care e plasa finală.
not-found.tsx funcționează diferit. Nu e un Error Boundary, ci o componentă tradusă printr-un cod de stare HTTP 404. Se declanșează doar atunci când apelezi explicit notFound() sau când utilizatorul accesează o rută care nu există în arbore.
error.tsx: prinderea erorilor pe segment
Cel mai utilizat fișier dintre cele trei. Îl plasezi în orice folder din app/ și prinde erorile din toate componentele de acolo și din subfoldere, până ajunge la următorul error.tsx care preia răspunderea. Trebuie marcat obligatoriu cu "use client", pentru că React Error Boundaries există doar pe client.
// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// În producție mesajul real e ascuns; logăm doar pe server.
console.error("Dashboard error:", error);
}, [error]);
return (
<div className="error-shell">
<h2>Ceva nu a mers bine în dashboard</h2>
<p>Cod referință: {error.digest ?? "n/a"}</p>
<button onClick={() => reset()}>Reîncearcă</button>
</div>
);
}
Două detalii pe care le-am ratat la început. error.digest e singura informație utilă pe care o ai despre eroare în producție (mesajul real e înlocuit cu „An error occurred" pentru securitate), iar reset() re-randează doar segmentul curent. Datele cache-uite cu fetch rămân aceleași, deci dacă eroarea vine dintr-un fetch eșuat, ai nevoie de router.refresh() din next/navigation ca să forțezi o re-rulare completă.
Plasarea contează enorm. Un error.tsx la nivelul app/dashboard/ nu va prinde erorile aruncate din app/dashboard/layout.tsx, pentru că layout-ul e părintele, nu copilul. Ca să prinzi eroarea din layout, ai nevoie de error.tsx în segmentul părinte. Dacă urmezi pattern-urile de rute paralele și interceptare, fiecare slot are propriul Error Boundary, ceea ce e foarte util pentru widget-uri care eșuează independent.
global-error.tsx pentru erorile din root layout
Singurul fișier de eroare care înlocuiește integral <html> și <body>. Motivul e simplu. Dacă eroarea apare în app/layout.tsx, nu mai există un layout funcțional care să găzduiască boundary-ul, deci componenta de fallback trebuie să fie un document HTML complet.
În producție, dacă o eroare ajunge la global-error.tsx, înseamnă că ceva fundamental a cedat: un provider greșit, o eroare de hidratare din root, o problemă de runtime cu un middleware. Tratează-l ca pe „last resort" și asigură-te că aici NU mai faci fetch-uri sau alte operații care pot eșua. Am fost prins de acest lucru pe un proiect anterior, când un provider de telemetrie eșua la inițializare și am încercat să-l reapelez tot din global-error.tsx. Rezultat: o buclă infinită de erori care a făcut browser-ul să înghețe.
not-found.tsx și funcția notFound()
Spre deosebire de cele două de mai sus, not-found.tsx nu e declanșat de excepții. E declanșat de două lucruri: (1) un apel explicit la funcția notFound() din next/navigation, sau (2) navigarea către o rută care nu există în arborele app/. În al doilea caz, doar app/not-found.tsx (rădăcina) răspunde; pentru rute necunoscute profund, e singurul activ.
Funcția notFound() aruncă efectiv o excepție tipată intern (NEXT_NOT_FOUND), pe care framework-ul o tratează ca un caz separat. Codul HTTP returnat e 404, ceea ce e corect pentru SEO și pentru crawlerele care altfel ar indexa pagini „goale". Dacă pui un not-found.tsx la nivel de segment (app/products/not-found.tsx), el va răspunde pentru apelurile notFound() din acel subarbore. Pentru rute complet inexistente, însă, doar cel din rădăcină se activează.
Reține un detaliu. params e acum un Promise în Next.js 16, ceea ce era doar opțional în 15. Dacă mai ai cod vechi care destrucurează direct params.slug fără await, vei primi un warning în dev și un eșec în build. Vezi ghidul Route Handlers din Next.js 16 pentru contextul complet despre params async.
Cum gestionezi erorile în Server Components vs Client Components?
Distincția aceasta m-a costat o săptămână de debugging când am migrat un proiect din pages router. În Server Components, o excepție aruncată în timpul randării e capturată de cel mai apropiat error.tsx, iar utilizatorul vede componenta de fallback fără ca eroarea să ajungă vreodată în browser-ul lui. În Client Components, comportamentul e identic doar dacă eroarea apare în timpul randării. Erorile din event handlers (de exemplu, un onClick) NU sunt prinse de Error Boundary și trebuie gestionate manual cu try/catch.
// app/cart/AddToCart.tsx (Client Component)
"use client";
import { useState, useTransition } from "react";
export function AddToCart({ productId }: { productId: string }) {
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
async function handleClick() {
setError(null);
startTransition(async () => {
try {
const res = await fetch(`/api/cart/${productId}`, { method: "POST" });
if (!res.ok) throw new Error("Server respinse cererea");
} catch (err) {
// Aceasta NU ar fi prinsă de error.tsx; trebuie gestionat aici.
setError(err instanceof Error ? err.message : "Eroare necunoscută");
}
});
}
return (
<>
<button onClick={handleClick} disabled={isPending}>Adaugă în coș</button>
{error && <p role="alert">{error}</p>}
</>
);
}
Pentru fetch-uri în Server Components, cea mai curată abordare e să arunci excepția și să lași error.tsx să o prindă. Pentru fetch-uri din Client Components, captează în try/catch și afișează inline, mai prietenos cu UX-ul decât o pagină întreagă de eroare.
Erori în Server Actions: pattern-ul return, nu throw
Aici am pierdut câteva zile bune. Server Actions sunt funcții serverless apelate din formulare. Dacă arunci o excepție, ea e prinsă de Next.js și transformată automat într-o eroare 500, iar utilizatorul ajunge la error.tsx. De obicei nu vrei asta: vrei mesaje inline („Email-ul există deja", „Parola e prea scurtă") afișate sub câmpul respectiv.
// app/auth/actions.ts
"use server";
import { z } from "zod";
const SignupSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export type SignupState = {
status: "idle" | "error" | "success";
errors?: Record<string, string>;
message?: string;
};
export async function signup(
prevState: SignupState,
formData: FormData
): Promise<SignupState> {
const parsed = SignupSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
});
if (!parsed.success) {
// RETURN, nu throw; așa rămâne în UI ca state al formularului.
return {
status: "error",
errors: parsed.error.flatten().fieldErrors as Record<string, string>,
};
}
try {
await createUser(parsed.data);
return { status: "success", message: "Cont creat" };
} catch (err) {
// Loghează intern; expune doar un mesaj sigur.
console.error("[signup]", err);
return { status: "error", message: "Nu am putut crea contul." };
}
}
Pe partea de client, folosești useActionState ca să citești SignupState și să afișezi erorile de validare unde aparțin. Acest pattern face zero apeluri către error.tsx pentru erori de domeniu, ceea ce e corect, pentru că o validare eșuată nu e un crash al aplicației. Mai multe despre acest flux în ghidul pentru Server Actions și validare cu Zod.
De ce nu prinde error.tsx erorile mele?
Honestly, întrebarea apare des. Top cinci motive în ordinea frecvenței:
Lipsește directiva "use client". Fără ea, Next.js îl tratează ca pe un Server Component și nu îl atașează ca Error Boundary. Compilatorul nu te avertizează; pur și simplu nu funcționează.
Eroarea e aruncată în layout.tsx de la același nivel. Un error.tsx nu prinde erorile din propriul layout, doar din componentele copil. Mută-l un nivel mai sus.
Eroarea apare într-un event handler din Client Component. Error Boundaries prind doar erori de randare. Folosește try/catch local.
Eroarea apare într-un useEffect. La fel. Efectele rulează după randare și nu sunt acoperite. Captează în efect.
Eroarea apare într-o Server Action. Vezi secțiunea de mai sus: adesea ar trebui să returnezi state, nu să arunci.
Logging și integrare cu Sentry în producție
Cum spuneam, în producție mesajul real e înlocuit cu un text generic. Nu e un bug, e o măsură de securitate pentru a nu scurge stack trace-uri sensibile. Singura ta legătură între ce vede utilizatorul și ce s-a întâmplat e error.digest, un hash scurt pe care Next.js îl loghează pe server odată cu eroarea completă.
Pentru a captura și erorile din Server Components (care nu trec niciodată prin error.tsx din perspectiva clientului), Sentry pune la dispoziție SDK-ul oficial pentru Next.js care patch-uiește runtime-ul Node și raportează automat. Configurarea via instrumentation.ts e recomandată și prinde erori înainte ca App Router să le vadă. Pentru detalii despre acest hook, vezi documentația Next.js pentru instrumentation.
Dacă lucrezi cu Drizzle ORM în Server Components, ține minte că o conexiune eșuată la baza de date va arunca o excepție generică. Împachetează queries critice în try/catch și transformă-le în erori cu context (cod de eroare, query etc.) înainte să le re-arunci, ca să găsești problema în Sentry.
Întrebări frecvente
Care e diferența dintre error.tsx și not-found.tsx?
error.tsx e un Error Boundary care prinde excepții aruncate în timpul randării și returnează HTTP 500. not-found.tsx răspunde la apelul notFound() sau la rute inexistente și returnează HTTP 404. Ambele sunt Client Components, dar declanșatorii și codurile de stare diferă fundamental.
Pot folosi error.tsx fără "use client"?
Nu. React Error Boundaries există doar pe client, iar Next.js cere obligatoriu directiva "use client" în orice fișier error.tsx și global-error.tsx. Fără ea, fișierul e compilat ca Server Component și nu e înregistrat ca boundary, iar erorile vor urca în arbore fără să fie prinse.
De ce nu se afișează global-error.tsx în development?
Pentru că overlay-ul de erori din next dev are prioritate, ca să nu pierzi stack trace-ul. global-error.tsx rulează doar în build de producție. Testează-l cu next build && next start, nu cu next dev.
Cum personalizez pagina 404 în Next.js 16?
Creează un fișier app/not-found.tsx care exportă default o componentă React. Va fi randată automat când utilizatorul accesează o rută inexistentă sau când apelezi notFound() din next/navigation. Pentru 404 specifice pe segment (de exemplu, produs negăsit), plasează not-found.tsx în acel folder.
Ce face funcția reset() din error.tsx?
reset() re-randează segmentul de rută unde a fost prinsă eroarea, încercând să restabilească starea anterioară. Nu invalidează datele cache-uite cu fetch. Dacă eroarea a venit dintr-un request eșuat, combină reset() cu router.refresh() sau cu revalidatePath() pe server pentru o recuperare reală.
Ghid practic pentru Turbopack în Next.js 16: cum activezi build-urile de producție, migrezi de la Webpack, configurezi loadere SVG/GraphQL, filesystem caching în CI și Subresource Integrity nativ. Cu benchmark-uri reale și capcane din producție.
Construiește o API REST completă cu Route Handlers în Next.js 16: GET/POST/PATCH/DELETE, validare Zod, autentificare prin cookies, caching opt-in, Edge Runtime și streaming SSE.
Rute paralele și de interceptare în Next.js 16: ghid practic pentru modaluri cu URL partajabil, dashboarduri cu streaming independent și pattern-uri avansate App Router cu exemple de cod complete.