Εισαγωγή: Γιατί η Σωστή Διαχείριση Σφαλμάτων Κάνει τη Διαφορά
Ας το παραδεχτούμε — κανείς δεν γράφει τέλειο κώδικα. APIs πέφτουν, βάσεις δεδομένων κάνουν timeout, χρήστες στέλνουν δεδομένα που δεν περιμέναμε, και μερικές φορές απλά κάτι πάει στραβά χωρίς κανέναν λόγο. Αυτό που ξεχωρίζει μια επαγγελματική εφαρμογή από ένα hobby project δεν είναι η απουσία σφαλμάτων — είναι ο τρόπος που τα διαχειρίζεται.
Στον κόσμο του Next.js App Router, η διαχείριση σφαλμάτων έχει εξελιχθεί δραματικά σε σχέση με τον παλιό Pages Router. Αντί για ένα γενικό _error.tsx αρχείο, τώρα έχουμε ένα πλήρες σύστημα βασισμένο σε αρχεία: error.tsx, global-error.tsx, not-found.tsx, global-not-found.tsx, και ισχυρά patterns για Server Actions με το useActionState του React 19.
Ειλικρινά, όταν πρωτοείδα αυτό το σύστημα, εντυπωσιάστηκα με το πόσο πιο λογικά είναι δομημένο σε σχέση με τον παλιό Pages Router.
Σε αυτόν τον οδηγό θα καλύψουμε τα πάντα για τη διαχείριση σφαλμάτων στο Next.js 16 — από βασικά error boundaries μέχρι production monitoring με Sentry. Αν έχετε διαβάσει τους προηγούμενους οδηγούς μας για Server Actions και Middleware, αυτό το άρθρο συμπληρώνει τη γνώση σας με ένα κρίσιμο κομμάτι του puzzle.
Η Αρχιτεκτονική Error Handling στο App Router
Το Next.js App Router χρησιμοποιεί ένα ιεραρχικό σύστημα διαχείρισης σφαλμάτων, βασισμένο σε file conventions. Κάθε route segment μπορεί να έχει τα δικά του αρχεία σφαλμάτων, δημιουργώντας ένα πολυεπίπεδο σύστημα που απομονώνει τα σφάλματα και κρατά την υπόλοιπη εφαρμογή λειτουργική.
Τα βασικά αρχεία είναι:
error.tsx— Πιάνει unexpected errors σε ένα συγκεκριμένο route segmentglobal-error.tsx— Πιάνει errors στο root layout (ολόκληρη η εφαρμογή)not-found.tsx— Χειρίζεται 404 σφάλματα μέσω τηςnotFound()functionglobal-not-found.tsx— Χειρίζεται unmatched URLs σε ολόκληρη την εφαρμογή
Η ιεραρχία λειτουργεί ως εξής: τα σφάλματα «ανεβαίνουν» (bubble up) στο πλησιέστερο error boundary. Αν ένα component σε μια nested σελίδα πετάξει error, πιάνεται από το error.tsx του ίδιου segment. Αν δεν υπάρχει εκεί, πηγαίνει στο parent segment, και ούτω καθεξής μέχρι το global-error.tsx. Σκεφτείτε το σαν ένα δίχτυ ασφαλείας — αν κάτι πέσει, το πιάνει το πλησιέστερο επίπεδο.
Το Αρχείο error.tsx: Route-Level Error Boundaries
Πώς Λειτουργεί
Το error.tsx είναι η πρώτη γραμμή άμυνας. Δημιουργεί ένα React Error Boundary γύρω από ένα route segment και τα children του. Όταν κάτι πάει στραβά — είτε σε Server Component είτε σε Client Component — αντί να κρασάρει ολόκληρη η σελίδα, εμφανίζεται ένα fallback UI.
Σημαντικό: Το error.tsx πρέπει πάντα να είναι Client Component. Δηλαδή χρειάζεται η directive "use client" στην αρχή — δεν υπάρχει δεύτερος τρόπος.
// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log στην κονσόλα ή σε εξωτερικό service
console.error("Dashboard error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<h2 className="text-2xl font-bold text-red-600">
Κάτι πήγε στραβά!
</h2>
<p className="text-gray-600">
Παρουσιάστηκε σφάλμα στο Dashboard.
</p>
{error.digest && (
<p className="text-sm text-gray-400">
Κωδικός σφάλματος: {error.digest}
</p>
)}
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Δοκιμάστε ξανά
</button>
</div>
);
}
Τα Props που Δέχεται
Το error.tsx δέχεται δύο props:
error— Το αντικείμενο Error. Στο development, περιέχει πλήρη πληροφορία. Στο production όμως, τα sensitive details αφαιρούνται για λόγους ασφαλείας και αντικαθίστανται με ένα generic μήνυμα. Τοerror.digestείναι ένα hash που μπορείτε να χρησιμοποιήσετε για αντιστοίχιση με τα server logs — πολύ χρήσιμο για debugging.reset— Μια function που κάνει re-render το τμήμα χωρίς πλήρη ανανέωση σελίδας. Ιδανική για transient errors (π.χ. μια στιγμιαία αποτυχία δικτύου).
Κρίσιμοι Περιορισμοί
Εδώ υπάρχουν μερικές «παγίδες» που πρέπει να γνωρίζετε:
- Δεν πιάνει errors από
layout.tsxήtemplate.tsxτου ίδιου segment. Αυτό είναι σχεδιασμένο έτσι — τα shared layouts (π.χ. navigation) πρέπει να παραμένουν ορατά ακόμα κι αν ένα child κάνει crash. Αν θέλετε να πιάσετε layout errors, πρέπει να τοποθετήσετε τοerror.tsxστο parent segment. - Δεν πιάνει errors σε event handlers. Τα Error Boundaries πιάνουν μόνο rendering errors. Για errors σε
onClick,onSubmitκ.λπ., χρειάζεστε manual try/catch μέσα στον handler. - Δεν πιάνει async errors εκτός rendering. Errors σε
setTimeoutή standalonefetchcalls (εκτός component rendering) δεν πιάνονται. Αυτό ξεγελάει αρκετούς developers.
Granular Error Handling: Πολλαπλά error.tsx
Μία από τις μεγαλύτερες δυνάμεις του App Router είναι ότι μπορείτε να τοποθετήσετε error.tsx σε διαφορετικά επίπεδα, δημιουργώντας ζώνες απομόνωσης. Ας δούμε ένα παράδειγμα:
app/
├── error.tsx # Πιάνει errors σε ολόκληρη την εφαρμογή
├── dashboard/
│ ├── error.tsx # Πιάνει errors μόνο στο dashboard
│ ├── page.tsx
│ ├── analytics/
│ │ ├── error.tsx # Πιάνει errors μόνο στο analytics
│ │ └── page.tsx
│ └── settings/
│ ├── error.tsx # Πιάνει errors μόνο στα settings
│ └── page.tsx
└── blog/
├── error.tsx # Πιάνει errors μόνο στο blog
└── [slug]/
├── error.tsx # Πιάνει errors σε συγκεκριμένο post
└── page.tsx
Αν το analytics panel πετάξει error, μόνο αυτό εμφανίζει fallback UI — το navigation, το settings panel, και τα υπόλοιπα τμήματα της σελίδας παραμένουν λειτουργικά. Αυτή η αρχιτεκτονική είναι ιδιαίτερα πολύτιμη σε dashboards που χρησιμοποιούν Parallel Routes.
Το Αρχείο global-error.tsx: Η Τελευταία Γραμμή Άμυνας
Το global-error.tsx πιάνει errors που δεν κατάφερε να χειριστεί κανένα άλλο error boundary — συγκεκριμένα, errors στο root layout (app/layout.tsx) ή στο root template.
Υπάρχει μια κρίσιμη διαφορά εδώ: επειδή αντικαθιστά πλήρως το root layout, πρέπει να περιέχει τα δικά του <html> και <body> tags. Αν το ξεχάσετε, θα πάρετε ένα αρκετά μπερδεμένο αποτέλεσμα.
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="el">
<body>
<div className="flex flex-col items-center justify-center min-h-screen gap-6">
<h1 className="text-3xl font-bold text-red-600">
Σοβαρό Σφάλμα Εφαρμογής
</h1>
<p className="text-gray-600 text-center max-w-md">
Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα.
Παρακαλούμε δοκιμάστε να ανανεώσετε τη σελίδα.
</p>
<button
onClick={() => reset()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg
hover:bg-blue-700 transition-colors"
>
Επαναφόρτωση
</button>
</div>
</body>
</html>
);
}
Μερικές σημαντικές παρατηρήσεις:
- Στο development, αντί για το
global-error.tsx, θα δείτε ένα error overlay με λεπτομέρειες debugging. Θέλετε να δείτε το πραγματικό fallback UI; Πρέπει να τεστάρετε σε production build. - Τα
metadataexports καιgenerateMetadataδεν υποστηρίζονται στοglobal-error.tsx. Αν χρειάζεστε δυναμικό title, χρησιμοποιήστε το React<title>component. - Ακόμα κι αν έχετε
global-error.tsx, είναι καλή πρακτική να έχετε και ένα rooterror.tsx. Ο λόγος; Τα περισσότερα errors θα χειρίζονται μέσα στο κανονικό layout, διατηρώντας navigation κ.λπ.
Διαχείριση 404 Σφαλμάτων: not-found.tsx και global-not-found.tsx
Το Αρχείο not-found.tsx
Το not-found.tsx χειρίζεται 404 σφάλματα — σελίδες ή πόρους που δεν υπάρχουν. Ενεργοποιείται όταν καλείτε τη συνάρτηση notFound() μέσα σε ένα route segment.
Ας δούμε ένα τυπικό σενάριο — ένα blog post που μπορεί να μην υπάρχει:
// 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}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return res.json();
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound(); // Ενεργοποιεί το not-found.tsx
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// app/blog/[slug]/not-found.tsx
import Link from "next/link";
export default function PostNotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<h2 className="text-2xl font-bold">Το άρθρο δεν βρέθηκε</h2>
<p className="text-gray-600">
Το άρθρο που αναζητάτε δεν υπάρχει ή έχει αφαιρεθεί.
</p>
<Link
href="/blog"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Επιστροφή στο Blog
</Link>
</div>
);
}
Μια σημαντική λεπτομέρεια: Η notFound() έχει προτεραιότητα σε σχέση με το error.tsx. Οπότε αν θέλετε πιο φιλικό μήνυμα (π.χ. «δεν βρέθηκε» αντί για «κάτι πήγε στραβά»), προτιμήστε τη notFound().
Επιπλέον, ένα global app/not-found.tsx πιάνει και τα unmatched URLs ολόκληρης της εφαρμογής — δηλαδή αν ο χρήστης πληκτρολογήσει ένα URL που δεν αντιστοιχεί σε κανένα route.
Το Αρχείο global-not-found.tsx (Νέο)
Αυτό είναι μια σχετικά νέα προσθήκη και λύνει ένα πραγματικό πρόβλημα: εφαρμογές με πολλαπλά root layouts. Αν η εφαρμογή σας χρησιμοποιεί route groups με ξεχωριστά layouts (π.χ. app/(admin)/layout.tsx και app/(shop)/layout.tsx), δεν υπάρχει ένα ενιαίο layout για να εμφανίσει 404 σελίδα. Εδώ μπαίνει το global-not-found.tsx.
Για να το ενεργοποιήσετε, προσθέστε τη ρύθμιση στο next.config.ts:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
globalNotFound: true,
},
};
export default nextConfig;
// app/global-not-found.tsx
export default function GlobalNotFound() {
return (
<html lang="el">
<body>
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-6xl font-bold text-gray-300">404</h1>
<p className="text-xl text-gray-600 mt-4">
Η σελίδα δεν βρέθηκε
</p>
<a href="/" className="mt-6 text-blue-600 hover:underline">
Επιστροφή στην αρχική
</a>
</div>
</body>
</html>
);
}
Σε αντίθεση με το not-found.tsx, το global-not-found.tsx δεν χρειάζεται rendering μέσα σε κάποιο layout — το Next.js το εμφανίζει απευθείας όταν κανένα route δεν ταιριάζει. Κομψό, σωστά;
Διαχείριση Σφαλμάτων σε Server Actions
Τα Server Actions είναι ένα από τα πιο δυνατά features του Next.js App Router, αλλά η διαχείριση σφαλμάτων τους χρειάζεται λίγη προσοχή. Υπάρχουν δύο κατηγορίες σφαλμάτων: αναμενόμενα (expected) και μη αναμενόμενα (unexpected).
Αναμενόμενα Σφάλματα με useActionState
Για σφάλματα που μπορείτε να προβλέψετε — validation errors, duplicate entries, permission denied — η σωστή προσέγγιση είναι να τα επιστρέφετε ως τιμές αντί να τα πετάτε ως exceptions. Εδώ λάμπει πραγματικά το useActionState hook του React 19.
// lib/actions/contact.ts
"use server";
import { z } from "zod";
const ContactSchema = z.object({
name: z.string().min(2, "Το όνομα πρέπει να έχει τουλάχιστον 2 χαρακτήρες"),
email: z.string().email("Μη έγκυρη διεύθυνση email"),
message: z.string().min(10, "Το μήνυμα πρέπει να έχει τουλάχιστον 10 χαρακτήρες"),
});
export type ContactFormState = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function submitContactForm(
prevState: ContactFormState,
formData: FormData
): Promise<ContactFormState> {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
// Validation με Zod
const validated = ContactSchema.safeParse(rawData);
if (!validated.success) {
return {
success: false,
message: "Σφάλμα επικύρωσης δεδομένων",
errors: validated.error.flatten().fieldErrors,
};
}
try {
// Αποθήκευση στη βάση δεδομένων
await saveToDatabase(validated.data);
return {
success: true,
message: "Το μήνυμά σας στάλθηκε με επιτυχία!",
};
} catch (err) {
return {
success: false,
message: "Αποτυχία αποστολής. Δοκιμάστε ξανά αργότερα.",
};
}
}
// app/contact/contact-form.tsx
"use client";
import { useActionState } from "react";
import { submitContactForm, type ContactFormState } from "@/lib/actions/contact";
const initialState: ContactFormState = {
success: false,
message: "",
};
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(
submitContactForm,
initialState
);
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name">Όνομα</label>
<input
id="name"
name="name"
type="text"
className="w-full border rounded px-3 py-2"
/>
{state.errors?.name && (
<p className="text-red-500 text-sm mt-1">
{state.errors.name[0]}
</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
className="w-full border rounded px-3 py-2"
/>
{state.errors?.email && (
<p className="text-red-500 text-sm mt-1">
{state.errors.email[0]}
</p>
)}
</div>
<div>
<label htmlFor="message">Μήνυμα</label>
<textarea
id="message"
name="message"
rows={4}
className="w-full border rounded px-3 py-2"
/>
{state.errors?.message && (
<p className="text-red-500 text-sm mt-1">
{state.errors.message[0]}
</p>
)}
</div>
{state.message && (
<div
className={`p-3 rounded ${
state.success
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{state.message}
</div>
)}
<button
type="submit"
disabled={isPending}
className="px-6 py-2 bg-blue-600 text-white rounded
hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Αποστολή..." : "Αποστολή"}
</button>
</form>
);
}
Αυτό το pattern έχει αρκετά πλεονεκτήματα:
- Τα validation errors εμφανίζονται δίπλα στα πεδία που αφορούν — πιο χρηστικό για τον τελικό χρήστη
- Η κατάσταση φόρτωσης (
isPending) χειρίζεται αυτόματα από το React - Η φόρμα λειτουργεί ακόμα κι αν απενεργοποιηθεί η JavaScript (progressive enhancement)
- Τα δεδομένα φόρμας διατηρούνται μετά από σφάλμα — κανείς δεν θέλει να ξαναγράψει ολόκληρη τη φόρμα
Μη Αναμενόμενα Σφάλματα σε Server Actions
Αν ένα Server Action πετάξει error (throw), αυτό πιάνεται από το πλησιέστερο error.tsx. Ο χρήστης βλέπει ένα γενικό «κάτι πήγε στραβά» και — εδώ είναι το χειρότερο — χάνει τα δεδομένα φόρμας. Γι' αυτό πάντα χρησιμοποιούμε try/catch μέσα στο Server Action και επιστρέφουμε structured errors αντί να πετάμε exceptions.
Η Παγίδα του redirect μέσα σε try/catch
Αυτό είναι ένα κλασικό bug που έχει «τσιμπήσει» σχεδόν κάθε Next.js developer τουλάχιστον μία φορά. Η redirect() function του Next.js λειτουργεί πετώντας ένα ειδικό error. Αν τη βάλετε μέσα σε try/catch, θα πιαστεί από το catch block!
// ΛΑΘΟΣ — Η redirect πιάνεται από το catch
"use server";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
try {
const post = await savePost(formData);
redirect(`/posts/${post.id}`); // Θα πιαστεί από το catch!
} catch (err) {
return { error: "Αποτυχία δημιουργίας" };
}
}
// ΣΩΣΤΟ — Η redirect είναι εκτός try/catch
"use server";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
let postId: string;
try {
const post = await savePost(formData);
postId = post.id;
} catch (err) {
return { error: "Αποτυχία δημιουργίας" };
}
redirect(`/posts/${postId}`);
}
Η λύση, όπως βλέπετε, είναι απλή: αποθηκεύστε το αποτέλεσμα μέσα στο try block και καλέστε τη redirect() εκτός.
Error Handling σε Server Components
Τα Server Components εκτελούνται αποκλειστικά στον server, πράγμα που σημαίνει ότι τα errors τους δεν μπορούν να χειριστούν απευθείας στον client. Η ιεραρχία error boundaries όμως δουλεύει κανονικά — τα errors «φτάνουν» στο πλησιέστερο error.tsx.
Ασφάλεια σε Production
Στο production, τα errors από Server Components καθαρίζονται αυτόματα. Αντί για λεπτομερή μηνύματα (που μπορεί να περιέχουν ευαίσθητες πληροφορίες, όπως connection strings ή stack traces), ο client λαμβάνει μόνο ένα generic μήνυμα και ένα digest — ένα hash που μπορείτε να αντιστοιχίσετε στα server logs σας.
// app/products/page.tsx
// Server Component — τα errors πιάνονται από error.tsx
async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 60 },
});
if (!res.ok) {
// Αυτό θα πιαστεί από το πλησιέστερο error.tsx
throw new Error("Αδυναμία φόρτωσης προϊόντων");
}
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product: any) => (
<div key={product.id} className="border rounded p-4">
<h3>{product.name}</h3>
<p>{product.price}€</p>
</div>
))}
</div>
);
}
Ο Σωστός Τρόπος: Προσεκτικό Error Handling
Αντί να πετάτε errors και να βασίζεστε στο error boundary, σκεφτείτε αν μπορείτε να χειριστείτε το error μέσα στο component με ένα graceful fallback. Αυτή η προσέγγιση δίνει καλύτερο UX:
// app/products/page.tsx
async function getProducts() {
try {
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 60 },
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
export default async function ProductsPage() {
const products = await getProducts();
if (!products) {
return (
<div className="text-center py-12">
<p className="text-gray-600">
Δεν ήταν δυνατή η φόρτωση των προϊόντων αυτή τη στιγμή.
</p>
<p className="text-sm text-gray-400 mt-2">
Δοκιμάστε να ανανεώσετε τη σελίδα.
</p>
</div>
);
}
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product: any) => (
<div key={product.id} className="border rounded p-4">
<h3>{product.name}</h3>
<p>{product.price}€</p>
</div>
))}
</div>
);
}
Η διαφορά; Αντί να αντικατασταθεί ολόκληρη η σελίδα με ένα error UI, εμφανίζεται ένα κομψό μήνυμα μέσα στη σελίδα ενώ τα υπόλοιπα στοιχεία (header, navigation, footer) παραμένουν ορατά. Πολύ πιο ήπιο για τον χρήστη.
Η Λειτουργία reset: Ανάκαμψη Χωρίς Refresh
Η reset() function που παρέχεται στο error.tsx είναι εξαιρετικά χρήσιμη για transient errors — σφάλματα δικτύου, temporary timeouts, ή race conditions. Μπορείτε ακόμα να υλοποιήσετε αυτόματο retry:
// app/dashboard/error.tsx
"use client";
import { useEffect, useState } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
// Auto-retry μία φορά μετά από 2 δευτερόλεπτα
if (retryCount === 0) {
const timer = setTimeout(() => {
setRetryCount(1);
reset();
}, 2000);
return () => clearTimeout(timer);
}
}, [retryCount, reset]);
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<h2 className="text-2xl font-bold text-red-600">
Σφάλμα φόρτωσης Dashboard
</h2>
{retryCount > 0 ? (
<>
<p className="text-gray-600">
Η αυτόματη επανάληψη απέτυχε.
</p>
<button
onClick={() => {
setRetryCount((c) => c + 1);
reset();
}}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Δοκιμάστε ξανά χειροκίνητα
</button>
</>
) : (
<p className="text-gray-500 animate-pulse">
Γίνεται αυτόματη επανάληψη...
</p>
)}
</div>
);
}
Προσοχή: Η reset() κάνει re-render μόνο το component tree κάτω από το error boundary. Αν το error προκαλείται από κάτι μόνιμο (π.χ. bug στον κώδικα), θα πιάσετε πάλι error — η reset δεν κάνει μαγικά.
Production Monitoring: Σύνδεση με Sentry
Σε production, δεν αρκεί να δείξετε ένα ωραίο fallback UI — πρέπει να γνωρίζετε ότι συνέβη σφάλμα, τι ακριβώς ήταν, και πώς να το αναπαράγετε. Ειλικρινά, χωρίς monitoring σε production, πετάτε στα τυφλά. Εδώ μπαίνει το Sentry (ή κάποιο παρόμοιο εργαλείο).
Εγκατάσταση
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs
Ο wizard δημιουργεί αυτόματα τα αρχεία sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts, και ρυθμίζει το instrumentation.ts. Αρκετά βολικό.
Σύνδεση error.tsx με Sentry
Ένα σημείο που πιάνει πολλούς εξ απήνης: αν χρησιμοποιείτε custom error.tsx αρχεία, το Sentry δεν πιάνει αυτόματα τα errors. Πρέπει να καλέσετε χειροκίνητα το Sentry.captureException():
// app/dashboard/error.tsx
"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Αποστολή στο Sentry
Sentry.captureException(error);
}, [error]);
return (
<div className="text-center py-12">
<h2 className="text-xl font-bold text-red-600">
Κάτι πήγε στραβά
</h2>
<button onClick={reset} className="mt-4 text-blue-600 hover:underline">
Δοκιμάστε ξανά
</button>
</div>
);
}
Structured Logging
Εκτός από error capturing, μπορείτε να στέλνετε structured logs για πιο λεπτομερές monitoring:
// Μέσα σε Server Action ή API route
import * as Sentry from "@sentry/nextjs";
Sentry.logger.info("Νέα παραγγελία", { orderId: "123", amount: 99.90 });
Sentry.logger.warn("Αργή απόκριση API", { endpoint: "/products", duration: 5200 });
Sentry.logger.error("Αποτυχία πληρωμής", { reason: "timeout", userId: "456" });
Πλήρη Αρχιτεκτονική: Τα Πάντα Μαζί
Λοιπόν, ας δούμε πώς δένουν όλα μαζί σε μια πραγματική εφαρμογή. Αυτή η δομή καλύπτει τα περισσότερα σενάρια:
app/
├── layout.tsx # Root layout
├── error.tsx # Πιάνει errors κάτω από root layout
├── global-error.tsx # Πιάνει errors στο root layout
├── not-found.tsx # Global 404 σελίδα
├── (shop)/
│ ├── layout.tsx # Shop layout
│ ├── products/
│ │ ├── page.tsx # Λίστα προϊόντων (Server Component)
│ │ ├── error.tsx # Error boundary για products
│ │ └── [id]/
│ │ ├── page.tsx # Σελίδα προϊόντος
│ │ ├── not-found.tsx # Το προϊόν δεν βρέθηκε
│ │ └── error.tsx # Error boundary ανά προϊόν
│ └── cart/
│ ├── page.tsx # Καλάθι (Client Component)
│ ├── error.tsx # Error boundary για cart
│ └── actions.ts # Server Actions με useActionState
├── (admin)/
│ ├── layout.tsx # Admin layout
│ └── dashboard/
│ ├── page.tsx
│ ├── error.tsx # Ανεξάρτητο error handling
│ └── @stats/
│ ├── page.tsx
│ └── error.tsx # Error boundary για stats panel
Κάθε τμήμα έχει τα δικά του error boundaries, δίνοντας τη δυνατότητα ένα panel να κρασάρει χωρίς να επηρεάσει τα υπόλοιπα. Αυτό κάνει τεράστια διαφορά στο UX, ειδικά σε μεγάλες εφαρμογές.
Βέλτιστες Πρακτικές: Checklist
Πριν κλείσουμε, ορίστε ένα γρήγορο checklist με τις βέλτιστες πρακτικές:
- Τοποθετήστε
error.tsxσε κάθε σημαντικό route segment — ειδικά σε dashboards και σελίδες με πολλαπλά data sources. - Έχετε πάντα ένα root
error.tsxκαι έναglobal-error.tsx— ως safety net. - Χρησιμοποιήστε
useActionStateγια expected errors σε φόρμες — αντί να πετάτε errors και να χάνετε τα δεδομένα φόρμας. - Μην βάζετε
redirect()μέσα σεtry/catch— βγάλτε τη redirect εκτός. Σοβαρά, αυτό πιάνει τον κόσμο συνεχώς. - Αντιμετωπίζετε τα errors ως δεδομένα, όχι ως exceptions — επιστρέφετε structured error objects.
- Συνδέστε monitoring (Sentry) — καλέστε
captureExceptionμέσα σταerror.tsxcomponents. - Τεστάρετε σε production build — πολλά error behaviors διαφέρουν μεταξύ dev και production.
- Χρησιμοποιήστε
not-found.tsxγια πόρους που δεν βρέθηκαν — αντί για γενικό error handling.
Συχνές Ερωτήσεις (FAQ)
Ποια είναι η διαφορά μεταξύ error.tsx και global-error.tsx στο Next.js;
Το error.tsx χειρίζεται errors σε ένα συγκεκριμένο route segment και τα children του, ενώ διατηρεί τα shared layouts (π.χ. navigation). Το global-error.tsx χειρίζεται errors στο root layout (app/layout.tsx) — πρέπει να περιέχει δικά του <html> και <body> tags γιατί αντικαθιστά πλήρως το root layout. Στην πράξη, χρειάζεστε και τα δύο: error.tsx ως κύρια γραμμή άμυνας, global-error.tsx ως τελευταίο safety net.
Γιατί το error.tsx πρέπει να είναι Client Component;
Τα React Error Boundaries βασίζονται σε lifecycle methods (componentDidCatch, getDerivedStateFromError) που υπάρχουν μόνο σε Client Components. Η directive "use client" είναι υποχρεωτική, δεν γίνεται αλλιώς. Αυτό δεν σημαίνει ότι εκτελείται μόνο client-side — κάνει render στον server (SSR) και hydrate στον client.
Πώς χειρίζομαι errors σε Server Actions χωρίς να χάνω τα δεδομένα φόρμας;
Χρησιμοποιήστε το useActionState hook του React 19 σε συνδυασμό με structured return values. Αντί να πετάτε exceptions (throw), επιστρέφετε ένα αντικείμενο με success, message, και errors fields. Έτσι η φόρμα δεν κάνει reset και τα validation errors εμφανίζονται δίπλα στα αντίστοιχα πεδία.
Μπορώ να χρησιμοποιήσω redirect μέσα σε try/catch block;
Όχι — η redirect() του Next.js λειτουργεί πετώντας ένα ειδικό error εσωτερικά. Αν βρίσκεται μέσα σε try/catch, θα πιαστεί από το catch block. Η σωστή προσέγγιση είναι να αποθηκεύσετε τα αποτελέσματα μέσα στο try block, και να καλέσετε τη redirect() μετά το try/catch.
Πώς στέλνω τα errors σε monitoring service όπως το Sentry;
Αν χρησιμοποιείτε custom error.tsx αρχεία, πρέπει να καλέσετε χειροκίνητα Sentry.captureException(error) μέσα σε ένα useEffect. Ο wizard του Sentry (npx @sentry/wizard -i nextjs) ρυθμίζει αυτόματα τα config αρχεία, αλλά δεν τροποποιεί τα custom error boundaries σας. Βεβαιωθείτε ότι έχετε ξεχωριστά config αρχεία για client, server, και edge runtime.