Next.js

Error Handling & Error Boundaries

24 min Lesson 20 of 40

Introduction to Error Handling

Robust error handling is essential for production applications. Next.js 13+ provides a comprehensive error handling system using special files like error.tsx, global-error.tsx, and not-found.tsx. This lesson covers implementing resilient error handling throughout your application.

Key Concept: Next.js uses React Error Boundaries under the hood, automatically wrapping route segments to catch errors and provide fallback UI without crashing the entire application.

Understanding Error Files

Next.js uses a file-based error handling system with specific naming conventions:

  • error.tsx: Catches errors in the route segment and nested children
  • global-error.tsx: Catches errors in the root layout (rare but critical)
  • not-found.tsx: Renders when a route or resource is not found
  • loading.tsx: Shows while content is loading (covered in previous lessons)

Creating an Error Boundary

Create an error.tsx file to catch errors within a route segment:

<!-- app/error.tsx -->
'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log error to error reporting service
    console.error('Error boundary caught:', error)
  }, [error])

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
        <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
          <svg
            className="w-8 h-8 text-red-600"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
            />
          </svg>
        </div>

        <h2 className="text-2xl font-bold text-gray-900 mb-2">
          Something went wrong!
        </h2>

        <p className="text-gray-600 mb-6">
          We apologize for the inconvenience. An error occurred while processing your request.
        </p>

        {process.env.NODE_ENV === 'development' && (
          <div className="mb-6 p-4 bg-gray-100 rounded-lg text-left">
            <p className="text-sm font-mono text-red-600 break-all">
              {error.message}
            </p>
            {error.digest && (
              <p className="text-xs text-gray-500 mt-2">
                Error ID: {error.digest}
              </p>
            )}
          </div>
        )}

        <button
          onClick={reset}
          className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
        >
          Try Again
        </button>

        <a
          href="/"
          className="block mt-4 text-sm text-blue-600 hover:text-blue-700"
        >
          Return to Home
        </a>
      </div>
    </div>
  )
}
Important: error.tsx must be a Client Component ('use client') because it uses React hooks like useEffect. The reset function allows users to retry the operation.

Global Error Handler

Create global-error.tsx to catch errors in the root layout:

<!-- app/global-error.tsx -->
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="min-h-screen flex items-center justify-center bg-gray-900 text-white px-4">
          <div className="max-w-md w-full text-center">
            <h1 className="text-4xl font-bold mb-4">Critical Error</h1>
            <p className="text-gray-400 mb-8">
              A critical error occurred. Please refresh the page or contact support.
            </p>

            {process.env.NODE_ENV === 'development' && (
              <div className="mb-6 p-4 bg-red-900 rounded-lg text-left">
                <p className="text-sm font-mono break-all">
                  {error.message}
                </p>
              </div>
            )}

            <button
              onClick={reset}
              className="px-6 py-3 bg-white text-gray-900 rounded-lg hover:bg-gray-100"
            >
              Try Again
            </button>
          </div>
        </div>
      </body>
    </html>
  )
}
Note: global-error.tsx must include <html> and <body> tags because it replaces the root layout when activated. It's rarely triggered but critical for catching errors in app/layout.tsx.

Not Found Pages

Create custom 404 pages using not-found.tsx:

<!-- app/not-found.tsx -->
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-4">
      <div className="max-w-lg w-full text-center">
        <h1 className="text-9xl font-bold text-blue-600 mb-4">404</h1>
        <h2 className="text-3xl font-bold text-gray-900 mb-4">
          Page Not Found
        </h2>
        <p className="text-gray-600 mb-8">
          Sorry, we couldn't find the page you're looking for.
          It might have been moved or deleted.
        </p>

        <div className="flex flex-col sm:flex-row gap-4 justify-center">
          <Link
            href="/"
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
          >
            Go to Home
          </Link>
          <button
            onClick={() => window.history.back()}
            className="px-6 py-3 bg-white text-gray-900 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
          >
            Go Back
          </button>
        </div>

        <div className="mt-12">
          <p className="text-sm text-gray-500 mb-4">Popular Pages:</p>
          <div className="flex flex-wrap justify-center gap-2">
            <Link href="/about" className="text-sm text-blue-600 hover:underline">
              About
            </Link>
            <span className="text-gray-300">•</span>
            <Link href="/blog" className="text-sm text-blue-600 hover:underline">
              Blog
            </Link>
            <span className="text-gray-300">•</span>
            <Link href="/contact" className="text-sm text-blue-600 hover:underline">
              Contact
            </Link>
          </div>
        </div>
      </div>
    </div>
  )
}

Programmatic Not Found

Trigger the not-found page programmatically using notFound():

<!-- app/posts/[slug]/page.tsx -->
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'

interface PageProps {
  params: {
    slug: string
  }
}

export default async function PostPage({ params }: PageProps) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })

  if (!post) {
    notFound()
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Route-Specific Error Pages

Create error pages for specific route segments:

<!-- app/dashboard/error.tsx -->
'use client'

import { useRouter } from 'next/navigation'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  const router = useRouter()

  return (
    <div className="p-8 max-w-lg mx-auto">
      <h2 className="text-2xl font-bold text-red-600 mb-4">
        Dashboard Error
      </h2>
      <p className="text-gray-600 mb-6">
        An error occurred while loading your dashboard. This could be due to a
        network issue or a temporary problem with our servers.
      </p>

      <div className="flex gap-4">
        <button
          onClick={reset}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          Retry
        </button>
        <button
          onClick={() => router.push('/')}
          className="px-4 py-2 bg-gray-200 text-gray-900 rounded-lg hover:bg-gray-300"
        >
          Go Home
        </button>
      </div>
    </div>
  )
}
<!-- app/dashboard/not-found.tsx -->
export default function DashboardNotFound() {
  return (
    <div className="p-8 text-center">
      <h2 className="text-2xl font-bold mb-4">Dashboard Page Not Found</h2>
      <p className="text-gray-600">
        The dashboard page you're looking for doesn't exist.
      </p>
    </div>
  )
}

Error Recovery Patterns

Implement intelligent error recovery strategies:

<!-- app/components/error-boundary-wrapper.tsx -->
'use client'

import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback: (error: Error, retry: () => void) => ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
  errorCount: number
}

export class ErrorBoundaryWrapper extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = {
      hasError: false,
      error: null,
      errorCount: 0
    }
  }

  static getDerivedStateFromError(error: Error): Partial<State> {
    return {
      hasError: true,
      error,
    }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error boundary caught:', error, errorInfo)

    // Send to error reporting service
    // reportError(error, errorInfo)

    this.setState(prevState => ({
      errorCount: prevState.errorCount + 1
    }))
  }

  retry = () => {
    // Limit retry attempts
    if (this.state.errorCount < 3) {
      this.setState({
        hasError: false,
        error: null,
      })
    } else {
      alert('Maximum retry attempts reached. Please refresh the page.')
    }
  }

  render() {
    if (this.state.hasError && this.state.error) {
      return this.props.fallback(this.state.error, this.retry)
    }

    return this.props.children
  }
}

API Error Handling

Handle errors in API routes and Server Actions:

<!-- app/api/posts/route.ts -->
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const createPostSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(1),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()

    // Validate input
    const validatedData = createPostSchema.parse(body)

    // Create post
    const post = await prisma.post.create({
      data: validatedData
    })

    return NextResponse.json(post, { status: 201 })

  } catch (error) {
    console.error('Create post error:', error)

    // Validation error
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        {
          error: 'Validation failed',
          issues: error.issues
        },
        { status: 400 }
      )
    }

    // Database error
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        return NextResponse.json(
          { error: 'A post with this title already exists' },
          { status: 409 }
        )
      }
    }

    // Generic error
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Server Action Error Handling

<!-- app/actions/post-actions.ts -->
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(1, 'Title is required'),
  content: z.string().min(10, 'Content must be at least 10 characters'),
})

export async function createPost(prevState: any, formData: FormData) {
  try {
    const rawData = {
      title: formData.get('title'),
      content: formData.get('content'),
    }

    // Validate
    const validatedData = postSchema.parse(rawData)

    // Create post
    const post = await prisma.post.create({
      data: validatedData
    })

    // Revalidate and redirect
    revalidatePath('/posts')
    redirect(`/posts/${post.slug}`)

  } catch (error) {
    console.error('Create post error:', error)

    if (error instanceof z.ZodError) {
      return {
        success: false,
        errors: error.flatten().fieldErrors,
        message: 'Validation failed'
      }
    }

    return {
      success: false,
      message: 'Failed to create post. Please try again.'
    }
  }
}

Client-Side Error Handling

<!-- app/components/safe-component.tsx -->
'use client'

import { useState, useEffect } from 'react'

export function SafeComponent() {
  const [data, setData] = useState(null)
  const [error, setError] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    fetchData()
  }, [])

  const fetchData = async () => {
    try {
      setIsLoading(true)
      setError(null)

      const response = await fetch('/api/data')

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const result = await response.json()
      setData(result)

    } catch (err) {
      console.error('Fetch error:', err)
      setError(err instanceof Error ? err.message : 'An error occurred')
    } finally {
      setIsLoading(false)
    }
  }

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (error) {
    return (
      <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
        <h3 className="text-red-800 font-semibold mb-2">Error</h3>
        <p className="text-red-600 mb-4">{error}</p>
        <button
          onClick={fetchData}
          className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
        >
          Retry
        </button>
      </div>
    )
  }

  return <div>{/* Render data */}</div>
}
Practice Exercise:
  1. Create custom error pages for different route segments (blog, dashboard, profile)
  2. Implement error logging to a service like Sentry or LogRocket
  3. Add retry logic with exponential backoff for failed API requests
  4. Create a custom error boundary that shows different UI based on error type
  5. Implement graceful degradation for non-critical features that fail
  6. Add user-friendly error messages for common error scenarios

Error Handling Best Practices

  • Always provide a way to recover from errors (retry button, navigation links)
  • Show different error messages in development vs production
  • Log errors to monitoring services for debugging
  • Use specific error pages for different route segments
  • Provide helpful context and next steps in error messages
  • Validate input early to catch errors before they propagate
  • Use proper HTTP status codes in API responses
  • Implement fallback UI for graceful degradation
  • Never expose sensitive error details to users in production

Summary

Effective error handling in Next.js uses error.tsx, global-error.tsx, and not-found.tsx files to create resilient applications. Always provide recovery mechanisms, log errors properly, show user-friendly messages, and implement graceful degradation. Good error handling improves user experience and makes debugging easier.