Next.js Redirect from / to another page
If your redirect works only after render, flickers, or fires in the wrong place, you are probably using the wrong redirect API for the job. Here is the exact mapping.
The symptom is usually one of these:
- the home page renders and then jumps away
- auth redirects flicker
redirect()works in one file but not another- a client-side button redirect gets copy-pasted into server code
The root cause is that Next.js has five redirect tools, and each one is for a different phase of the request.
The shortest correct answer is:
- use
redirect()for request-time server redirects - use
useRouter().push()in a client event handler - use
redirects()innext.config.jsfor static path rules
If / should always redirect, use next.config.js#
This is the cleanest answer when the rule is fixed.
// next.config.mjs
const nextConfig = {
async redirects() {
return [
{
source: '/',
destination: '/dashboard',
permanent: false,
},
]
},
}
export default nextConfigThis happens before rendering, so there is no flash of the home page first.
If / redirects based on auth or data, use redirect()#
In the App Router, this is the server-side pattern:
// app/page.tsx
import { redirect } from 'next/navigation'
export default async function HomePage() {
const isLoggedIn = false
if (isLoggedIn) {
redirect('/dashboard')
}
redirect('/login')
}The current Next.js docs say redirect() returns a 307 by default, and returns 303 when used in a Server Action.
If the redirect happens after a button click, use useRouter()#
This is for event handlers in client components, not for server rendering.
'use client'
import { useRouter } from 'next/navigation'
export default function GoToDashboardButton() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}If you do not need imperative navigation, keep using <Link />.
The mistake that causes most bad redirects#
People often use the client pattern for a server problem:
// Avoid this for root-route redirect logic
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
useEffect(() => {
router.push('/dashboard')
}, [router])
return null
}This works, but it is the wrong tool for a route-level redirect. The user briefly loads /, then navigates away. Search crawlers and users both get a worse experience.
Another easy mistake: calling redirect() inside try#
The docs explicitly note that redirect() throws. If you wrap it inside try/catch, you can accidentally swallow the navigation. In a Route Handler the same mistake shows up as Error: NEXT_REDIRECT in your logs and a redirect to the wrong place, or a 500; the redirect() caught inside a Route Handler page reproduces each case and shows the fix.
Use this shape instead:
import { redirect } from 'next/navigation'
export async function createProject() {
let id: string
try {
id = 'abc123'
} catch (error) {
throw error
}
redirect(`/projects/${id}`)
}Which redirect API should you pick?#
- Known path rule for every request:
redirects()innext.config.js - Request-time condition in server code:
redirect() - Canonical URL changed permanently:
permanentRedirect() - Click handler in client UI:
useRouter().push()
That mapping is the part most articles skip, and it is why the same redirect can feel broken in three different ways.
For adjacent routing gotchas, keep these open:
- Next.js 15 Middleware: Complete Guide to Auth, Rate Limiting, A/B Testing, and Edge Logic
- Next.js Server Actions vs API Routes: When to Use Each
- Fix Next.js revalidatePath Not Working in Server Actions
References#
- Stack Overflow: Next.js Redirect from / to another page
- Next.js docs: Redirecting
- Next.js docs:
redirectsinnext.config.js
Redirecting / conditionally, before the page ever renders#
The three tools above cover a fixed rule (next.config.js), a server-side condition
inside a Server Component or Server Action (redirect()), and a client click handler
(useRouter().push()). There's a fourth case none of them fit well: a condition you
need to check on every request to / — a cookie, a device hint, an A/B bucket — where
you don't want the home page's Server Component to start rendering first.
That's what Middleware is for. It runs at the edge, before routing decides which page to render, and it can inspect cookies and headers directly:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has('session')
if (request.nextUrl.pathname === '/' && !hasSession) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: '/',
}The trade-off versus redirect() inside app/page.tsx: Middleware never lets the root
page's Server Component start executing at all for a redirected request, which is
cheaper when the page does real data fetching you'd rather skip entirely. The trade-off
versus next.config.js redirects(): Middleware can read cookies and headers per
request, while redirects() can only match on the URL itself.
The most common way this quietly fails isn't a bad condition — it's the matcher
config not matching the route you expect, so the redirect never fires and you're left
debugging the wrong function. That specific failure mode, and how to confirm whether
Middleware is even running for a given path, is covered in
Next.js Middleware Not Running in Vercel Production.
Why the redirect silently drops your query string#
A redirect from /?ref=partner-x to /dashboard sometimes keeps ?ref=partner-x and
sometimes doesn't, depending on which of the three tools above did the redirecting —
and that inconsistency is a common source of lost attribution data.
With redirects() in next.config.js, query parameters on the incoming request that
aren't already captured by a named path segment in source are appended automatically
to whatever destination you configured — you don't have to do anything to preserve
them, and you don't need to name them in source unless you want to read their values
there.
With redirect() from next/navigation, there's no such passthrough, because you're
constructing the destination string yourself. If the incoming request had a query
string you want to carry forward, you have to read it and append it explicitly:
import { redirect } from 'next/navigation'
export default async function HomePage({
searchParams,
}: {
searchParams: Promise<{ ref?: string }>
}) {
const { ref } = await searchParams
redirect(ref ? `/dashboard?ref=${encodeURIComponent(ref)}` : '/dashboard')
}Skip this and any campaign or referral parameter that landed on / is gone the moment
the redirect fires — the destination page never receives it. For the general mechanics
of reading searchParams in the App Router (Server Components get a promise, Client
Components get a hook), see
How to get query string parameters in Next.js.
Further reading: Fix: Next.js middleware not triggering. Also see Fix: useRouter query undefined on first render.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix Error: NEXT_REDIRECT logged in a Route Handler catch
Reproduced on Next.js 16.4.2: why redirect() inside try/catch logs Error: NEXT_REDIRECT, sends users to the wrong URL or returns a 500, and the three fixes that hold.
Next.js Query String Params: searchParams + useRouter
If `router.query` examples keep breaking on you, the problem is usually that you're mixing App Router and Pages Router APIs. Here is the exact fix for each case.
Fix: useRouter query undefined on first render
Why Next.js hydrates a dynamic Pages Router page with an empty query object, how router.isReady fixes it, and why the App Router does not have this exact problem.


