How to Fix: redirect() error caught on client

5 min read

The bug is not that redirect() fails. The bug is that you are treating a successful redirect like a normal return value, then accidentally catching the special framework error that Next.js throws internally to perform navigation.

Understanding the Root Cause

In Next.js App Router, redirect() does not behave like a typical function that returns a response object. It works by throwing a special internal error that Next.js intercepts and converts into a navigation event.

That implementation detail matters a lot inside Server Actions. If your action wraps logic in a broad try/catch block, the internal redirect signal gets caught like any other thrown error. Once that happens, your code may log it, return it, or rethrow the wrong thing, and the client ends up seeing an error instead of clean navigation.

This is why the reproduction behaves strangely:

  • The form submits correctly.
  • The server action reaches redirect().
  • Next.js throws its internal redirect exception.
  • Your catch block intercepts it.
  • The client sees a caught error such as a redirect-related exception instead of a normal redirect flow.

In short, redirect is implemented as control flow, not business error handling.

This pattern is common in modern framework internals. Similar behavior exists with notFound() and other framework-level navigation helpers. They are intentionally thrown so rendering can stop immediately and transition to the next route state.

Step-by-Step Solution

The fix is to avoid swallowing the redirect signal. The safest approach is to move redirect() outside the try/catch block, or only catch the specific errors you actually want to handle.

1. Problematic pattern

This version catches the redirect exception and turns framework control flow into an application error.

'use server'

import { redirect } from 'next/navigation'

export async function submitAction() {
  try {
    await saveData()
    redirect('/success')
  } catch (error) {
    console.error(error)
    throw error
  }
}

Handle only the code that can fail, then call redirect() after the risky work completes.

'use server'

import { redirect } from 'next/navigation'

export async function submitAction() {
  try {
    await saveData()
  } catch (error) {
    console.error('Failed to save data:', error)
    throw new Error('Unable to submit form')
  }

  redirect('/success')
}

This works because the redirect() call is no longer inside the catchable block.

3. Alternative fix: rethrow redirect errors

If you truly need a wider try/catch, do not suppress framework navigation errors. Re-throw them immediately.

'use server'

import { redirect } from 'next/navigation'
import { isRedirectError } from 'next/dist/client/components/redirect'

export async function submitAction() {
  try {
    await saveData()
    redirect('/success')
  } catch (error) {
    if (isRedirectError(error)) {
      throw error
    }

    console.error('Application error:', error)
    throw new Error('Unable to submit form')
  }
}

Use this pattern carefully. In most cases, the cleaner design is still to keep redirect() outside the catch block.

4. Client component usage

If you call the server action from a form or button, you usually do not need client-side redirect handling. Let Next.js perform the navigation automatically.

'use client'

import { submitAction } from './actions'

export function MyForm() {
  return (
    <form action={submitAction}>
      <button type="submit">Submit</button>
    </form>
  )
}

If you manually invoke the action inside client code and wrap it in try/catch, be aware that redirect behavior can still surface in unexpected ways depending on how the action is called and rendered.

5. Best practice architecture

  • Use try/catch only around operations that can fail, such as database writes or API calls.
  • Call redirect() only after success is confirmed.
  • Do not convert redirect control flow into a user-facing error state.
  • If you must catch broadly, detect and rethrow redirect errors.
'use server'

import { redirect } from 'next/navigation'

export async function createPost(formData) {
  let postId

  try {
    postId = await db.post.create({
      data: {
        title: formData.get('title')
      }
    })
  } catch (error) {
    console.error('Database write failed:', error)
    return { message: 'Could not create post' }
  }

  redirect(`/posts/${postId.id}`)
}

This pattern is usually the most maintainable because it separates application failure handling from framework navigation flow.

Common Edge Cases

1. Catching notFound() the same way

notFound() also uses framework-controlled throwing. If you catch it broadly, you can break expected routing behavior just like with redirect().

2. Returning error objects and redirecting in the same action

A server action should have a clear contract. Either:

  • return a value the UI can render, or
  • redirect on success

Mixing both patterns often creates confusing control flow and inconsistent client behavior.

3. Redirecting after partial side effects

If your action performs multiple writes, make sure the redirect happens only after the operation is fully complete. Otherwise, users may land on a page that assumes data exists when it only partially saved.

4. Importing unstable internal helpers

The isRedirectError helper can be useful, but it comes from an internal path. Framework internals may shift across versions. Prefer the structural fix of moving redirect() outside the catch whenever possible.

5. Client-side manual invocation

If you call a server action from custom client event handlers instead of a plain form action, redirects may feel less intuitive. In those cases, test the flow carefully and consider whether router.push() on the client is more appropriate for that interaction.

6. Error boundaries showing redirect exceptions

If redirect errors escape incorrectly through your component tree, error boundaries or logging tools may report them as failures. That usually indicates redirect control flow was intercepted somewhere it should not have been.

FAQ

Why does Next.js throw an error for redirect() instead of returning normally?

Because it needs to stop rendering immediately and signal the router to navigate. Throwing is an internal control flow mechanism, not a sign that your application logic failed.

Should I always avoid try/catch in Server Actions?

No. You should still catch real failures like database, validation, or network errors. The key is to avoid catching framework navigation helpers unless you plan to rethrow them.

What is the simplest fix for this issue?

Move redirect() outside the try/catch block. Do your async work inside the catchable section, then redirect only after success.

The main takeaway is simple: redirect() is not a normal return path in Next.js. Treat it like framework-owned control flow, keep it out of broad error handlers, and the client-side redirect error disappears.

Leave a Reply

Your email address will not be published. Required fields are marked *