redirect()

Interrupts rendering and sends the user to another URL with a 307, or 303 after a Server Function.

Signature
redirect(path: string, type?: 'replace' | 'push'): never
  • Added in Next.js v13.0.0

Parameters

ParameterTypeDescription
$pathstringThe URL to send the user to. Accepts a relative path, an absolute path, or a fully qualified external URL.
$typeoptional'replace' | 'push'= 'replace' ('push' in Server Actions)How the browser history stack is updated. Defaults to 'replace' everywhere except Server Actions, where it defaults to 'push'. Has no effect in Server Components. The RedirectType object exports the same two values.

Return value

neverDoes not return a value. It throws a NEXT_REDIRECT control-flow error, so execution stops at the call site and TypeScript narrows anything checked before it.

Errors

Always throws a NEXT_REDIRECT error by design; a surrounding try/catch will catch it and silently cancel the navigation.

Changelog

  • PHP v13.0.0redirect introduced.

Reach for redirect() when a request should end somewhere else: an unauthenticated visitor sent to /login, a slug alias pointed at its canonical URL, or the detail page a form submission just created.

It works by throwing a NEXT_REDIRECT exception that Next.js catches at the segment boundary. Two consequences follow. Nothing after the call runs, so return redirect(...) is unnecessary — the TypeScript never return type tells the checker the same thing, and a value you null-checked before the call stays narrowed afterwards. And a surrounding try/catch swallows the exception, silently cancelling the navigation. Call it outside the try block, or pass the caught error through unstable_rethrow() first.

Which status code you get

307 by default, which preserves the request method — unlike a 302, which rewrites a POST into a GET. A progressive-enhancement form submission to a Server Function gets 303 instead, so the browser follows up with a GET. When JavaScript is available, a Server Action performs a client-side navigation rather than an HTTP redirect at all. Inside a streaming response, Next.js emits a meta tag to trigger the redirect on the client.

For redirects that should happen before rendering starts, use next.config.js redirects or a proxy instead.

Example

Example · typescript
import { redirect } from 'next/navigation'

async function fetchTeam(id: string) {
  const res = await fetch(`https://api.example.com/teams/${id}`)
  if (!res.ok) return undefined
  return res.json()
}

export default async function Profile({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const team = await fetchTeam(id)

  if (!team) {
    redirect('/login')
  }

  return <h1>{team.name}</h1>
}

When to use it

  • Send a visitor without a session to the login page from a Server Component
  • Navigate to the newly created record's page at the end of a Server Function
  • Point a legacy or aliased path at its canonical URL during rendering
  • Bounce a Route Handler caller to an external OAuth provider

Frequently asked questions

Why does my redirect not work inside try/catch?

Because redirect() signals the navigation by throwing a NEXT_REDIRECT error, and your catch block absorbs it. Move the call outside the try block, or call unstable_rethrow(err) at the top of the catch so framework errors pass through before you handle your own.

Do I need to write return redirect() in Next.js?

No. redirect() is typed as returning never, so TypeScript already knows execution stops there and the code below is unreachable. Adding return changes nothing at runtime and can confuse a function's declared return type.

Why is the status code 307 instead of 302?

307 preserves the original request method, while 302 causes many browsers to rewrite a POST into a GET. Server Action form submissions are the exception and use 303, because there the browser is meant to follow up with a GET.

More examples

Redirecting at the end of a Server Function

Order matters. `redirect()` throws, so `revalidatePath('/posts')` has to come first or the index page would never be invalidated. In a Server Action the default history behaviour is `push`, so the browser back button returns to the form. A JavaScript-enabled submission gets a client-side navigation; a form submitted without JavaScript gets a 303 response.

Example · typescript
// app/posts/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'

export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: { title: String(formData.get('title')) },
  })

  // Revalidate first — code after redirect() never runs.
  revalidatePath('/posts')

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

Forcing replace instead of push with RedirectType

Passing `RedirectType.replace` overrides the Server Action default of `push`, so the checkout form is removed from the history stack and the user cannot navigate back into a half-completed payment. In a Server Component this argument would be ignored entirely.

Example · typescript
// app/checkout/actions.ts
'use server'

import { redirect, RedirectType } from 'next/navigation'
import { chargeCard } from '@/lib/payments'

export async function pay(formData: FormData) {
  await chargeCard(String(formData.get('token')))

  // replace, so the back button cannot return to the checkout form
  redirect('/checkout/confirmed', RedirectType.replace)
}

Keeping the call outside try/catch in a Route Handler

The try block wraps only the fallible work and computes a destination string; the redirect itself happens after it. Had `redirect(destination)` been written inside the try, the catch would have absorbed NEXT_REDIRECT and the handler would fall through to an empty response instead of a 307.

Example · typescript
// app/api/session/route.ts
import { redirect } from 'next/navigation'
import { exchangeCode } from '@/lib/auth'

export async function GET(request: Request) {
  const code = new URL(request.url).searchParams.get('code')
  let destination = '/login?error=1'

  try {
    const session = await exchangeCode(code)
    destination = session.returnTo ?? '/dashboard'
  } catch {
    // swallow only the auth failure
  }

  // outside the try block, so NEXT_REDIRECT is never caught
  redirect(destination)
}

Discussion

  • Be the first to comment on this lesson.