redirect() & notFound()
Besides Link and useRouter, Next.js provides two special functions for controlling navigation and 404 behavior directly from server-side code: redirect() and notFound(). Both work by throwing a special internal signal that Next.js catches to produce the right response.
redirect()
redirect(), imported from next/navigation, can be called inside Server Components, Server Actions, and Route Handlers to send the user to a different URL. By default it issues a temporary redirect.
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { getCurrentUser } from '@/lib/auth'
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect('/login')
}
return <h1>Welcome, {user.name}</h1>
}redirect() in a Server Action
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const post = await db.post.create({ data: { title } })
redirect(`/blog/${post.id}`)
}This is often simpler than fetching in a Client Component and calling router.push afterward — the redirect happens right where the mutation completes, on the server.
permanentRedirect()
When a URL has moved for good — say, a page was renamed and you want search engines and browsers to update their records — use permanentRedirect() instead. It issues a 308 status code rather than the 307 that redirect() uses.
Function | Status Code | Meaning |
|---|---|---|
redirect() | 307 (Temporary Redirect) | This URL points elsewhere for now; re-check it later. |
permanentRedirect() | 308 (Permanent Redirect) | This URL has permanently moved; update your bookmarks/links. |
import { permanentRedirect } from 'next/navigation'
export default function OldBlogPage({ params }: { params: { slug: string } }) {
permanentRedirect(`/articles/${params.slug}`)
}notFound()
notFound(), also from next/navigation, tells Next.js to render the nearest not-found.tsx boundary and return a 404 status. It's the right tool whenever a requested resource genuinely doesn't exist — an invalid id, a deleted post, a slug with no match.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
if (!post) {
notFound()
}
return <article>{post.title}</article>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <h1>That post could not be found.</h1>
}Config-Based Redirects
For simple, static redirects that don't depend on runtime data — like retiring an old marketing URL — it's often cleaner to declare them in next.config.js rather than adding logic to a page.
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-pricing',
destination: '/pricing',
permanent: true,
},
]
},
}Choosing the Right Tool
Static, unconditional redirect known ahead of time -> next.config.js redirects().
Redirect depends on runtime data (auth state, form result, lookup) -> redirect() or permanentRedirect().
Resource genuinely does not exist -> notFound().
Redirect triggered by a user click in a Client Component -> useRouter().push()/replace() instead.
Together with Link and useRouter, redirect() and notFound() round out the App Router's navigation toolkit — giving you a way to control routing from server-side logic as naturally as you control it from the client.