Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

  • Cause: redirect() from next/navigation does not return a response. It throws an error with the message NEXT_REDIRECT, and Next.js converts that error into a 307 only when it escapes your handler. Inside try, your own catch gets it first.
  • Fix in a Route Handler: return NextResponse.redirect(new URL('/dashboard', request.url)). It is a plain return value, so try/catch cannot intercept it.
  • If you want to keep redirect(): call it after the try/catch, or put unstable_rethrow(error) on the first line of the catch.
  • Reproduced on Next.js 16.4.2 with Node v24.21.0 in the official node:24-alpine image, 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:

ts
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:

text
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):

text
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:

text
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:

ts
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 doesResult observed on 16.4.2
logs it, then calls redirect('/')307 to / (the first target is lost)
logs it, returns nothing500 No response is returned from route handler
unstable_rethrow(error) first307 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:

js
import { redirect } from 'next/navigation'
 
export async function GET() {
  try {
    redirect('/dashboard')
  } catch (error) {
    console.error('caught:', error.message)
  }
}

The server printed:

text
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:

ts
// 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:

ts
// 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":

ts
// 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:

bash
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:3000/api/auth

You 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:

text
⨯ Error: URL is malformed "/dashboard". Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urls

The 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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote