Back to Blog

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.

Viprasol Tech Team
May 17, 2027
12 min read

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

ScopeTeamTimelineCost Range
error.tsx + not-found.tsx1 devHalf a day$150โ€“300
global-error.tsx + AppError classes1 devHalf a day$150โ€“300
Pino structured logging + request ID1 dev1 day$300โ€“600
Sentry integration + session replay1 dev1 day$300โ€“600

See Also


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.tsx with useEffect Sentry capture, error.digest display, and reset() retry button
  • not-found.tsx at root and per-route-segment level, triggered by notFound()
  • global-error.tsx with full <html> wrapper (replaces root layout)
  • AppError, NotFoundError, UnauthorizedError, ValidationError class hierarchy
  • withErrorHandling wrapper 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.

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.