Keep redirect() out of try
redirect() and notFound() throw; call them outside try/catch.
Next.js docsredirect · Behavior
Do NOT wrap navigation APIs in try-catch. They throw special errors that Next.js handles internally.
From next-best-practices, error-handling.md.
Saved, yet the form fails
Press Publish twice, or tick Step manually.
redirect() inside tryBad
redirect() after tryGood
'use server'
import { redirect, unstable_rethrow } from 'next/navigation'
// Option 1: navigate after the try
export async function createPost(formData: FormData) {
let post
try {
post = await db.post.create({ data: { title: formData.get('title') } })
} catch {
return { error: 'Failed to create post' }
}
redirect(`/posts/${post.id}`)
}
// Option 2: rethrow Next.js errors first
export async function createPost(formData: FormData) {
try {
const post = await db.post.create({ data: { title: formData.get('title') } })
redirect(`/posts/${post.id}`)
} catch (error) {
unstable_rethrow(error)
return { error: 'Something went wrong' }
}
}
One catch eats five responses
Pick an API and compare the responses.
Wrapped in try/catchBad
Outside try/catchGood
307 asks again; 308 never does
Visit, remove the redirect on the server, visit again.
redirect() · 307DefaultpermanentRedirect 308Migrations onlyimport { redirect, permanentRedirect } from 'next/navigation'
// 307: the default
redirect('/login')
// 308: browsers cache it
permanentRedirect('/blog/new-slug')
Why it matters, pros and cons
Why it matters
They throw a special error, and a catch-all mistakes it for a failure.
Pros
- The catch only handles real failures.
- Real 404, 401 and 403 status codes.
- No false errors, so no duplicate submits.
Cons
- Hoisting out of
tryneeds aletoutside it. unstable_rethrowis unstable and must come first.forbidden()needsexperimental.authInterrupts.