Next.js Error Handling: error.tsx, not-found.tsx, Global Errors, and Structured Logging
Implement comprehensive error handling in Next.js App Router. Covers error.tsx reset boundaries, not-found.tsx with notFound(), global-error.tsx for root layout errors, structured error logging with Pino, Sentry integration, and error classification.
Next.js App Router provides a layered error handling system: error.tsx catches errors within a route segment, not-found.tsx handles missing resources, and global-error.tsx catches errors that escape the root layout. Most teams implement error.tsx but miss the other two โ leaving 404s and root-level errors showing a blank page.
This guide covers the complete error handling stack with structured logging and Sentry integration.
error.tsx โ Route Segment Error Boundary
// app/dashboard/error.tsx โ catches errors thrown in dashboard routes
"use client"; // Must be a Client Component
import { useEffect } from "react";
import { AlertTriangle, RefreshCw } from "lucide-react";
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void; // Re-renders the segment that errored
}
export default function DashboardError({ error, reset }: ErrorPageProps) {
useEffect(() => {
// Log to error tracking on mount
console.error("[dashboard] Caught error:", {
message: error.message,
digest: error.digest, // Next.js error ID (correlates server and client logs)
stack: error.stack,
});
// Send to Sentry
if (typeof window !== "undefined") {
import("@sentry/nextjs").then(({ captureException }) => {
captureException(error, {
tags: { segment: "dashboard", digest: error.digest },
});
});
}
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-[400px] px-4 text-center">
<AlertTriangle className="w-12 h-12 text-red-400 mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Something went wrong
</h2>
<p className="text-gray-500 text-sm max-w-md mb-6">
An error occurred while loading this page. This has been reported to our team.
{error.digest && (
<span className="block mt-1 font-mono text-xs text-gray-400">
Error ID: {error.digest}
</span>
)}
</p>
<button
onClick={reset} // Tries to re-render โ clears transient errors
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-semibold rounded-lg hover:bg-blue-700"
>
<RefreshCw className="w-4 h-4" />
Try again
</button>
</div>
);
}
not-found.tsx โ 404 Pages
// app/not-found.tsx โ global 404 (shown when notFound() is called)
import Link from "next/link";
import { FileQuestion } from "lucide-react";
export default function NotFoundPage() {
return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center">
<FileQuestion className="w-16 h-16 text-gray-300 mb-6" />
<h1 className="text-4xl font-bold text-gray-900 mb-2">404</h1>
<p className="text-lg text-gray-500 mb-2">Page not found</p>
<p className="text-sm text-gray-400 max-w-sm mb-8">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex gap-3">
<Link
href="/dashboard"
className="px-4 py-2 bg-blue-600 text-white text-sm font-semibold rounded-lg hover:bg-blue-700"
>
Go to Dashboard
</Link>
<Link
href="/"
className="px-4 py-2 bg-gray-100 text-gray-700 text-sm font-semibold rounded-lg hover:bg-gray-200"
>
Home
</Link>
</div>
</div>
);
}
// Route-specific not-found:
// app/projects/[id]/not-found.tsx โ shown when notFound() called in that segment
export default function ProjectNotFound() {
return (
<div className="text-center py-20">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Project not found</h2>
<p className="text-gray-500 mb-6">
This project may have been deleted or you may not have access.
</p>
<Link href="/projects" className="text-blue-600 hover:underline text-sm">
โ Back to projects
</Link>
</div>
);
}
// app/projects/[id]/page.tsx โ trigger not-found
import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { auth } from "@/auth";
export default async function ProjectPage({ params }: { params: { id: string } }) {
const session = await auth();
const project = await prisma.project.findFirst({
where: {
id: params.id,
workspaceId: session!.user.workspaceId,
},
});
// Triggers not-found.tsx in this segment
if (!project) notFound();
return <ProjectView project={project} />;
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
global-error.tsx โ Root Layout Errors
// app/global-error.tsx โ catches errors thrown in the root layout itself
// (e.g., auth provider failure, database connection error in layout)
"use client";
import { useEffect } from "react";
interface GlobalErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function GlobalError({ error, reset }: GlobalErrorProps) {
useEffect(() => {
// Sentry capture for root-level errors
import("@sentry/nextjs").then(({ captureException }) => {
captureException(error, { tags: { level: "global" } });
});
}, [error]);
// global-error MUST include <html> and <body> tags
// because it replaces the entire root layout
return (
<html lang="en">
<body>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
fontFamily: "system-ui, sans-serif",
textAlign: "center",
padding: "1rem",
}}
>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, marginBottom: "0.5rem" }}>
Something went seriously wrong
</h1>
<p style={{ color: "#6b7280", marginBottom: "1.5rem", maxWidth: "400px" }}>
A critical error occurred. Our team has been notified.
{error.digest && (
<span style={{ display: "block", marginTop: "0.5rem", fontFamily: "monospace", fontSize: "0.75rem", color: "#9ca3af" }}>
Error ID: {error.digest}
</span>
)}
</p>
<button
onClick={reset}
style={{
padding: "0.5rem 1rem",
background: "#2563eb",
color: "white",
border: "none",
borderRadius: "0.5rem",
cursor: "pointer",
fontWeight: 600,
}}
>
Try again
</button>
</div>
</body>
</html>
);
}
Structured Logging with Pino
// lib/logger.ts
import pino from "pino";
const isDev = process.env.NODE_ENV === "development";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
// Pretty print in development, JSON in production
transport: isDev
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
// Redact sensitive fields from all log lines
redact: ["req.headers.authorization", "req.headers.cookie", "password", "token"],
base: {
env: process.env.NODE_ENV,
service: "viprasol-app",
version: process.env.npm_package_version,
},
});
// Request-scoped child logger with request ID
export function createRequestLogger(requestId: string, userId?: string) {
return logger.child({ requestId, userId });
}
// middleware.ts โ attach request ID to all requests
import { NextResponse, type NextRequest } from "next/server";
import { nanoid } from "nanoid";
export function middleware(req: NextRequest) {
const requestId = nanoid(12);
const res = NextResponse.next();
// Pass request ID to server components via headers
res.headers.set("x-request-id", requestId);
return res;
}
// lib/errors.ts โ classify errors for structured logging
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly context?: Record<string, unknown>,
) {
super(message);
this.name = "AppError";
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, "NOT_FOUND", 404, { resource, id });
}
}
export class UnauthorizedError extends AppError {
constructor(reason?: string) {
super(reason ?? "Unauthorized", "UNAUTHORIZED", 401);
}
}
export class ValidationError extends AppError {
constructor(message: string, fields?: Record<string, string>) {
super(message, "VALIDATION_ERROR", 400, { fields });
}
}
// Route handler error wrapper
export function withErrorHandling<T>(
handler: () => Promise<T>
): Promise<T | Response> {
return handler().catch((err) => {
if (err instanceof AppError) {
return Response.json(
{ error: err.message, code: err.code, context: err.context },
{ status: err.statusCode }
);
}
logger.error({ err }, "Unhandled error in route handler");
return Response.json({ error: "Internal server error" }, { status: 500 });
});
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Sentry Integration
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
// Don't send to Sentry in development
enabled: process.env.NODE_ENV === "production",
beforeSend(event) {
// Strip PII from error events
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}
return event;
},
});
// sentry.client.config.ts
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
// Replay for session recording (optional โ privacy implications)
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
replaysOnErrorSampleRate: 0.1, // Capture replay for 10% of errors
});
Cost and Timeline Estimates
| Scope | Team | Timeline | Cost Range |
|---|---|---|---|
| error.tsx + not-found.tsx | 1 dev | Half a day | $150โ300 |
| global-error.tsx + AppError classes | 1 dev | Half a day | $150โ300 |
| Pino structured logging + request ID | 1 dev | 1 day | $300โ600 |
| Sentry integration + session replay | 1 dev | 1 day | $300โ600 |
See Also
- React Error Boundaries
- Next.js Middleware Auth Patterns
- Node.js Performance Profiling
- AWS CloudWatch Logs Insights
- Next.js App Router Caching Strategies
Working With Viprasol
Unhandled errors that show blank pages destroy user trust. Our team implements the full Next.js error handling stack: error.tsx boundaries with digest-correlated Sentry captures, notFound() triggers with context-appropriate 404 pages, global-error.tsx with its required <html> wrapper, and Pino structured logging with request IDs that let you trace a user report to an exact log line.
What we deliver:
error.tsxwithuseEffectSentry capture,error.digestdisplay, andreset()retry buttonnot-found.tsxat root and per-route-segment level, triggered bynotFound()global-error.tsxwith full<html>wrapper (replaces root layout)AppError,NotFoundError,UnauthorizedError,ValidationErrorclass hierarchywithErrorHandlingwrapper for route handlers- Pino logger with
redact,createRequestLogger(requestId, userId), and middleware request ID injection
Talk to our team about your error monitoring setup โ
Or explore our web development services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.