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.
TL;DR#
- Cause:
redirect()fromnext/navigationdoes not return a response. It throws an error with the messageNEXT_REDIRECT, and Next.js converts that error into a 307 only when it escapes your handler. Insidetry, your owncatchgets it first. - Fix in a Route Handler: return
NextResponse.redirect(new URL('/dashboard', request.url)). It is a plain return value, sotry/catchcannot intercept it. - If you want to keep
redirect(): call it after thetry/catch, or putunstable_rethrow(error)on the first line of thecatch. - Reproduced on Next.js 16.4.2 with Node v24.21.0 in the official
node:24-alpineimage, 22 October 2026.
The error and what it really means#
The Stack Overflow question behind this page has an auth Route Handler at app/api/auth/route.ts that looks like this:
import { redirect } from 'next/navigation';
export async function GET(req: Request) {
try {
redirect('/dashboard');
} catch (error) {
console.log(error);
redirect('/');
}
}The asker's terminal printed:
Error: NEXT_REDIRECT
at getRedirectError (webpack-internal:///(sc_server)/./node_modules/next/dist/client/components/redirect.js:40:19)
at redirect (webpack-internal:///(sc_server)/./node_modules/next/dist/client/components/redirect.js:46:11)
at GET (webpack-internal:///(sc_server)/./app/api/auth/route.ts:23:66)Running the same handler on Next.js 16.4.2 prints the lines below (the code frame between them is trimmed):
Error: NEXT_REDIRECT
at GET (app/api/so/route.js:5:13)
digest: 'NEXT_REDIRECT;replace;/dashboard;307;'
GET /api/so 307 in 835ms (next.js: 401ms, application-code: 433ms)Two details matter. First, this is not a crash: it is the output of the asker's own console.log(error). Second, the response is a 307, but its Location header is /, not /dashboard. The client in the reproduction saw:
GET /api/so -> 307 location=/So the handler "works", yet every request lands on the fallback URL, and the log fills with an error that looks fatal. The second redirect('/') in the catch is the only reason anything redirects at all.
Why it happens#
The redirect API reference says it plainly: "redirect throws an error so it should be called outside the try block when using try/catch statements", and for this exact case, "In Server Actions and Route Handlers, redirect should be called outside the try block when using try/catch statements."
The source shows how. In redirect.ts at v16.4.2, redirect() is typed never and builds an Error whose message is NEXT_REDIRECT, with the target packed into a digest string:
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError
error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`Then it throws that error. Nothing is sent yet. The Route Handler module (app-route/module.ts) catches whatever your GET throws, checks it with isRedirectError(err), reads the URL from the digest, and only then builds new Response(null, ...) with a Location header and the status from the digest. The digest is the whole protocol: if the error never reaches that code, there is no redirect.
That is why the digest format differs between the two logs above. In the question's log it was NEXT_REDIRECT;replace;/dashboard; the current source appends the status code and a trailing separator. Code that parses the digest by hand breaks across versions, which is one more reason not to inspect it yourself.
What happens next depends on what your catch does with the error:
catch block does | Result observed on 16.4.2 |
|---|---|
logs it, then calls redirect('/') | 307 to / (the first target is lost) |
| logs it, returns nothing | 500 No response is returned from route handler |
unstable_rethrow(error) first | 307 to /dashboard |
The middle row is the one that hurts in production. In the reproduction the handler below (app/api/swallow/route.js) returned a 500:
import { redirect } from 'next/navigation'
export async function GET() {
try {
redirect('/dashboard')
} catch (error) {
console.error('caught:', error.message)
}
}The server printed:
caught: NEXT_REDIRECT
⨯ Error: No response is returned from route handler '[project]/app/api/swallow/route.js'. Expected a Response object but received 'undefined' (method: GET, url: /api/swallow). Ensure you return a `Response` or a `NextResponse` in all branches of your handler.The redirect error was consumed, the function fell off its end, and the module's finalizeResponse rejected the undefined return value.
Fix#
Pick one of the three. The first is the idiomatic choice in a Route Handler; the other two keep redirect().
1. Return NextResponse.redirect() from the handler#
A Route Handler already has to return a Response. NextResponse.redirect() produces one, so the redirect becomes a value you return, not an exception you throw. It is safe in try, in catch, or both:
// app/api/auth/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
// ...exchange the code, set the session cookie...
return NextResponse.redirect(new URL('/dashboard', request.url))
} catch (error) {
console.error('auth callback failed:', error)
return NextResponse.redirect(new URL('/login?error=callback', request.url))
}
}new URL('/dashboard', request.url) resolves the path against the incoming request, so the same code works on localhost:3000 and on your production domain. In the reproduction this returned 307 location=http://localhost:3000/dashboard. The status defaults to 307 in the NextResponse source; pass a second argument such as 303 after a form POST if you want the browser to follow with a GET.
This is also the answer the highest-voted reply on the question gives for API routes.
2. Decide inside try, redirect after it#
If you prefer redirect(), keep the try for the work that can fail and compute the destination there. Call redirect() once, after the block:
// app/api/auth/route.ts
import { redirect } from 'next/navigation'
export async function GET() {
let target = '/'
try {
// ...the call that can fail...
target = '/dashboard'
} catch (error) {
console.error('auth callback failed:', error)
}
redirect(target)
}Reproduced result: 307 location=/dashboard, and nothing in the log. This is the shape the Next.js redirecting guide uses in its own examples.
3. Rethrow framework errors with unstable_rethrow()#
When the code inside try calls helpers that may themselves call redirect() or notFound(), moving the call out is not always possible. unstable_rethrow rethrows the errors Next.js owns and ignores the rest. The docs say it "should be called at the top of the catch block, passing the error object as its only argument":
// app/api/auth/route.ts
import { redirect, unstable_rethrow } from 'next/navigation'
export async function GET() {
try {
redirect('/dashboard')
} catch (error) {
unstable_rethrow(error)
console.error('real error:', error)
return Response.json({ error: 'failed' }, { status: 500 })
}
}Reproduced result: 307 location=/dashboard. Note the name: the API carries an Unstable badge in the docs, so check the reference after a major upgrade. Avoid importing isRedirectError from next/dist/... to do the same by hand; it is an internal module path, not a public export.
Verify the fix#
Do not test a redirect in the browser first: the browser follows it and you only see the final page. Ask for the raw response instead:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:3000/api/authYou want a 307 and the URL you intended. Then check the terminal running next dev: after the fix, a successful request prints only the request line, with no Error: NEXT_REDIRECT block. If Error: NEXT_REDIRECT still appears, some catch in the call chain is logging the error before it reaches the framework, often a shared try/catch wrapper around all your handlers. Add unstable_rethrow(error) at the top of that wrapper's catch.
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.
The relative URL trap after switching to NextResponse#
The most common follow-up error after fix 1 is passing a bare path. NextResponse.redirect('/dashboard') does not resolve against the request. It validates the string with new URL() and throws:
⨯ Error: URL is malformed "/dashboard". Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urlsThe request then fails with a 500. The fix is the new URL(path, request.url) form shown above. redirect() from next/navigation accepts relative paths; NextResponse.redirect() does not, so the two are not drop-in replacements. A related relative-URL failure appears when server code calls fetch('/api/...'), covered in Error: only absolute urls are supported.
If the route returns 404 or 405 before any of this runs, the handler is not being reached at all; app/api 404 vs 405 covers the route.js file name and named GET export that the App Router needs.
Pages, Server Actions and the same throw#
The same thrown error drives redirect() in Server Components and Server Actions, so the same rule applies there. In a Server Action the status becomes 303 for a form submission without JavaScript, per the redirect docs. For choosing between redirect(), useRouter().push() and next.config.js redirects in pages, see Next.js redirect from / to another page. For a Server Action that writes data and then redirects, Fix revalidatePath not working in Server Actions shows the revalidatePath then redirect order, and the Server Actions with Supabase guide covers validation and error handling around those actions.
A Route Handler is different from those contexts in one useful way: it already returns a Response. That makes NextResponse.redirect() the natural tool there, and keeps redirect() for places where you cannot return one. The reproduction for this page ran six handlers in one next dev session on Next.js 16.4.2, node:24-alpine, 22 October 2026: the question's code verbatim, a swallowing catch, redirect() after the block, unstable_rethrow, NextResponse.redirect() with an absolute URL, and with a relative one. Every status and log line quoted above comes from that run.
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.
Continue Reading
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.
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.


