Error Handling in Actions

Return Errors, Don't Throw (Mostly)

Error Handling in Actions

For expected validation and business errors, return a result object. For unexpected failures, throw — Next.js surfaces those to the nearest `error.tsx`.

4 min read Level 2/5 #nextjs#errors#actions
What you'll learn
  • Return `{ error }` from validation failures
  • Throw for true exceptions (DB down, etc.)
  • Read returned errors via `useActionState`

There are two kinds of errors in a Server Action. Expected ones (the user typed an invalid email) belong in your return value. Unexpected ones (the database is unreachable) belong in a throw. Treating them the same is the mistake.

Returning Expected Errors

'use server'

type State = { error?: string; ok?: boolean }

export async function save(prev: State, form: FormData): Promise<State> {
  const email = String(form.get('email') ?? '')
  if (!email.includes('@')) return { error: 'Email is invalid' }

  const exists = await db.users.findByEmail(email)
  if (exists) return { error: 'Email already in use' }

  await db.users.create({ email })
  return { ok: true }
}

The action completes successfully; the result describes whether the business rule passed. useActionState makes the result available to the form.

Throwing for Unexpected Failures

'use server'

export async function save(prev: State, form: FormData): Promise<State> {
  try {
    await db.users.create({ email: String(form.get('email')) })
    return { ok: true }
  } catch (e) {
    // Connection refused, constraint violation we cannot recover from, etc.
    throw e // surfaces error.tsx
  }
}

Thrown errors travel up to the nearest error.tsx, which is a Client Component boundary the user can retry from.

Why Not Throw for Everything

If you throw on validation failures, you lose the form state and the user gets bounced to a generic error page. Worse, the error message you wrote might leak data you do not want to show. Returning errors keeps the form intact and gives you full control over the copy.

Rule of Thumb

  • Expected (validation, conflict, “wrong password”): return { error }
  • Unexpected (crashes, DB down, programming bugs): throw
Rendering Strategies →