Next.js Error Handling — error.tsx and not-found.tsx

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Error Handling — error.tsx and not-found.tsx

Next.js provides built-in error handling with error.tsx and not-found.tsx files for graceful error management and user-friendly error pages.

Creating Error Boundaries

An error.tsx file creates an error boundary for its route segment:

// app/blog/error.tsx
'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to monitoring service
    console.error('Blog error:', error)
  }, [error])

  return (
    <div className="space-y-4">
      <h1>Something went wrong!</h1>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

When an error occurs in /blog or its children, this component renders.

Error Types Caught

Error boundaries catch:

  • Runtime errors in Server Components
  • Errors in async functions
  • Errors in Client Components

They don't catch:

  • Errors in layout.tsx (use layout error boundary)
  • Errors in middleware
  • Server-side rendering issues

Layout Error Boundaries

Create separate error boundaries for layouts:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}
// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h1>Critical Error</h1>
        <p>{error.message}</p>
        <button onClick={reset}>Reset</button>
      </body>
    </html>
  )
}

global-error.tsx catches errors in the root layout.

404 Pages

Create a not-found.tsx for missing pages:

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

export default function NotFound() {
  return (
    <div className="text-center py-12">
      <h1 className="text-4xl font-bold">404</h1>
      <p className="text-xl">Page not found</p>
      <Link href="/" className="text-blue-600">
        Return home
      </Link>
    </div>
  )
}

Triggering not-found

Use the notFound() function to trigger the 404 page:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const post = await db.posts.findUnique({ where: { slug } })
  return post
}

export default async function BlogPostPage({ params }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound() // Shows app/not-found.tsx or nearest not-found.tsx
  }

  return <article>{post.content}</article>
}

Nested Error Boundaries

Create error boundaries at multiple levels:

// app/layout.tsx
export default function RootLayout({ children }) {
  return <html>{children}</html>
}
// app/error.tsx
'use client'

export default function AppError({ error, reset }) {
  return (
    <div>
      <h1>App Error</h1>
      <button onClick={reset}>Retry</button>
    </div>
  )
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
  return <div className="dashboard">{children}</div>
}
// app/dashboard/error.tsx
'use client'

export default function DashboardError({ error, reset }) {
  return (
    <div>
      <h1>Dashboard Error</h1>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )
}

Errors in the dashboard route trigger the dashboard error boundary, not the app-level one.

Real-World Error Handling

// app/api-client/error.tsx
'use client'

import { useEffect } from 'react'
import { captureException } from '@sentry/nextjs'

export default function Error({ error, reset }) {
  useEffect(() => {
    // Send to error tracking service
    captureException(error)

    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error(error)
    }
  }, [error])

  return (
    <div className="border-l-4 border-red-500 bg-red-50 p-4">
      <h2 className="font-bold">Error Loading Data</h2>
      <p className="text-sm">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
      >
        Try Again
      </button>
    </div>
  )
}

Handling Async Errors in Server Actions

Server Actions automatically handle errors:

// app/actions.ts
'use server'

export async function createPost(title: string, content: string) {
  try {
    const post = await db.posts.create({ data: { title, content } })
    return { success: true, post }
  } catch (error) {
    return { success: false, error: 'Failed to create post' }
  }
}
// app/blog/new/page.tsx
'use client'

import { createPost } from '@/app/actions'
import { useState } from 'react'

export default function NewPostPage() {
  const [error, setError] = useState('')

  async function handleSubmit(formData: FormData) {
    setError('')

    const result = await createPost(
      formData.get('title') as string,
      formData.get('content') as string
    )

    if (!result.success) {
      setError(result.error)
    }
  }

  return (
    <form action={handleSubmit}>
      {error && <p className="text-red-600">{error}</p>}
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  )
}

Error Recovery Patterns

Pattern 1: Automatic Retry

'use client'

import { useState, useTransition } from 'react'

export function DataFetcher() {
  const [retryCount, setRetryCount] = useState(0)
  const [isPending, startTransition] = useTransition()

  const handleRetry = () => {
    if (retryCount &lt; 3) {
      setRetryCount(prev => prev + 1)
      startTransition(() => {
        // Retry logic
      })
    }
  }

  return (
    &lt;div&gt;
      &lt;p&gt;Attempt {retryCount}&lt;/p&gt;
      &lt;button onClick={handleRetry} disabled={isPending}&gt;
        Retry
      &lt;/button&gt;
    &lt;/div&gt;
  )
}

Pattern 2: Fallback Content

export default async function DataSection() {
  try {
    const data = await fetchData()
    return <div>{data}</div>
  } catch (error) {
    return <div>Using cached data</div>
  }
}

Testing Error Boundaries

// components/error-boundary.test.tsx
import { render } from '@testing-library/react'
import ErrorBoundary from './error-boundary'

test('displays error message', () => {
  const { getByText } = render(
    <ErrorBoundary>
      <ComponentThatThrows />
    </ErrorBoundary>
  )

  expect(getByText(/something went wrong/i)).toBeInTheDocument()
})

FAQ

Q: Do error boundaries catch all errors? A: No, they catch rendering errors. Errors in event handlers need try-catch blocks.

Q: Can I customize the 404 page per route? A: Yes, create not-found.tsx at any level. Nearest not-found.tsx wins.

Q: Should I always create error.tsx? A: Create it for segments where errors are likely or need custom handling. Generic errors fall back to parent boundaries.


Proper error handling creates resilient, user-friendly applications.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro