Error Handling in Next.js – Expected Errors, Uncaught Exceptions, and Error Boundaries

Next.js categorizes errors into expected errors and uncaught exceptions. This article explains how to handle form errors with useActionState, show custom 404 pages with notFound, and define error boundaries for unexpected exceptions. It also covers event handler errors, startTransition exceptions, and global error handling in the root layout.

expected errorerror boundaryuseActionStatenotFound

~4 min read • Updated Oct 25, 2025

Types of Errors in Next.js


Errors fall into two categories:

  • Expected errors: e.g. form validation failures or failed fetch requests
  • Uncaught exceptions: unexpected bugs or crashes during rendering

Handling Expected Errors


In Server Functions


Use useActionState to handle form errors. Instead of throwing, return structured error messages:


// app/actions.ts
'use server'
export async function createPost(prevState, formData) {
  const title = formData.get('title')
  const content = formData.get('content')
  const res = await fetch('https://api.vercel.app/posts', {
    method: 'POST',
    body: { title, content },
  })
  if (!res.ok) {
    return { message: 'Failed to create post' }
  }
}

In Client Forms


// app/ui/form.tsx
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'

const initialState = { message: '' }

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, initialState)
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      {state?.message && <p>{state.message}</p>}
      <button disabled={pending}>Create Post</button>
    </form>
  )
}

In Server Components


Check fetch responses and conditionally render fallback UI:


// app/page.tsx
export default async function Page() {
  const res = await fetch('https://...')
  if (!res.ok) return 'There was an error.'
  return '...'
}

Custom 404 Pages


Use notFound() to trigger a 404 UI:


// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts'

export default async function Page({ params }) {
  const post = getPostBySlug(params.slug)
  if (!post) notFound()
  return <div>{post.title}</div>
}

// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return <div>404 - Page Not Found</div>
}

Handling Uncaught Exceptions


Error Boundaries


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


// app/dashboard/error.tsx
'use client'
import { useEffect } from 'react'

export default function Error({ error, reset }) {
  useEffect(() => {
    console.error(error)
  }, [error])
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Event Handler Errors


Error boundaries don’t catch event handler errors. Use useState to store and display them:


// app/ui/button.tsx
'use client'
import { useState } from 'react'

export function Button() {
  const [error, setError] = useState(null)
  const handleClick = () => {
    try {
      throw new Error('Exception')
    } catch (reason) {
      setError(reason)
    }
  }
  if (error) return <p>Error occurred</p>
  return <button onClick={handleClick}>Click me</button>
}

startTransition Errors


Errors inside startTransition bubble to the nearest error boundary:


// app/ui/button.tsx
'use client'
import { useTransition } from 'react'

export function Button() {
  const [pending, startTransition] = useTransition()
  const handleClick = () =>
    startTransition(() => {
      throw new Error('Exception')
    })
  return <button onClick={handleClick}>Click me</button>
}

Global Error Handling


Use global-error.tsx in the root app directory to catch layout-level errors. Must include <html> and <body> tags:


// app/global-error.tsx
'use client'
export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

Conclusion


Next.js offers robust error handling tools. With useActionState, notFound(), error boundaries, and global-error.tsx, you can build resilient applications that gracefully recover from both expected and unexpected failures.


Written & researched by Dr. Shahin Siami