Redirecting from a Server Action means calling the redirect function from next/navigation once the action's work is done. Inside an action it performs a client-side navigation when JavaScript is available, and falls back to an HTTP 303 response for progressive enhancement.
The safe pattern is to call it last, outside any try/catch block.
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
const posts = new Map<string, string>()
export async function createPost(formData: FormData) {
const id = crypto.randomUUID()
posts.set(id, formData.get('title')?.toString() ?? '')
redirect(`/posts/${id}`)
}The action saves the post, then sends the visitor to the new record's page. With JavaScript running the browser navigates without a full reload.
A visitor without JavaScript gets a 303 response instead, which tells the browser to follow the redirect with a GET. That is what keeps the POST from being replayed against the destination.
Why redirect throws
redirect works by throwing a NEXT_REDIRECT error. Next.js catches it and turns it into a navigation, which is why the function never returns normally.
Because it throws, two rules follow. Do not wrap redirect in a try/catch, because your catch block would swallow the navigation and leave the visitor on the old page. And do not put work after redirect, because that code never runs.
Treat it like a return statement that never comes back. TypeScript agrees: redirect is typed as never, so you do not write return redirect().
If you genuinely need a try/catch around the work, put the redirect after the catch block rather than inside the try.
Order matters with revalidation
When the destination page shows data you just changed, revalidate before redirecting. redirect stops execution, so a revalidation placed after it is dead code that never runs.
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function savePost(formData: FormData) {
const id = formData.get('id')?.toString()
revalidatePath('/posts')
redirect(`/posts/${id}`)
}The revalidation invalidates /posts first, then the navigation sends the visitor there, so the destination renders with fresh data instead of the cached copy.
Calling redirect first would skip the revalidation entirely and leave the list stale. This is the most common ordering bug in Server Actions, and it fails quietly, because the redirect itself still works and only the data looks wrong. See revalidating data after a Server Action for the revalidation calls.
Push vs replace
The history behavior differs by context. In a Server Action, redirect defaults to push, which adds a new history entry. Everywhere else it defaults to replace, which swaps the current entry.
| Context | Default type |
|---|---|
| Server Action | push |
| Server Component or Route Handler | replace |
You can override the default by passing RedirectType as the second argument, imported from next/navigation alongside redirect. The type parameter has no effect in Server Components.
For a permanent redirect that returns 308 instead of 307, use permanentRedirect. See redirect vs permanentRedirect for when each fits.
When to use useRouter instead
The line is not server versus client. redirect runs while rendering, in Server Components and Client Components alike, and it runs inside Server Actions and Route Handlers. What it cannot do is run from an event handler.
So a button onClick is the one place it will not work. Use the useRouter hook there instead and call router.push, which is the same navigation driven from the browser. See useRouter in the App Router for the rest of that API.
A common mistake is calling redirect from an onClick and wondering why nothing happens. redirect also accepts absolute URLs, so you can send a visitor to an external site after a form, such as a hosted checkout page.
Redirect works outside actions too
redirect is not limited to Server Actions. It runs while rendering Server Components and inside Route Handlers, where it sends a 307 temporary redirect by default.
// app/api/posts/route.ts
import { redirect } from 'next/navigation'
export async function GET() {
redirect('/login')
}The route handler above sends visitors to the login page, and a Server Component that finds a missing record can call redirect before returning any UI. Because both run on the server, they can decide the destination from request data.
The status codes are worth knowing. A 307 preserves the original request method, unlike the older 302 that many browsers rewrite from POST to GET, and permanentRedirect returns 308 for the permanent case. Server Action form submissions are the exception that uses 303, so the browser follows with a GET.
Rune AI
Key Insights
- redirect sends the visitor to another URL after an action finishes.
- It throws NEXT_REDIRECT, so never wrap it in try/catch.
- Call revalidatePath or updateTag before redirect.
- Server Actions default to push, other contexts default to replace.
- Use useRouter instead of redirect in event handlers.
Frequently Asked Questions
Can I call redirect in a Client Component event handler?
Why does code after redirect not run?
Does redirect use push or replace in a Server Action?
Conclusion
Call redirect at the end of a Server Action, outside any try/catch, after your revalidation. It performs a client-side navigation when JavaScript is available and a 303 redirect otherwise, so the visitor lands on fresh data.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.