Errors fall into two categories:
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' }
}
}// 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>
)
}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 '...'
}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>
}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>
)
}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>
}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>
}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>
)
}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.