return $this->redirectToRoute vs redirect() — Why a PHP Reflex Cancels Your Navigation in Next.js
In PHP, a redirect is a return value. In Next.js, it's an exception. Mix the two in a try/catch and the redirect silently disappears.
In PHP, a redirect is a return value. You call a method, it returns a Response object, you return that response, and the framework handles the rest. In Next.js, redirect() from next/navigation works the opposite way: it throws a special error (NEXT_REDIRECT), and the framework catches it at the boundary to send an HTTP 307 response to the browser.
If you come from PHP, this difference is invisible in the code. You write what looks like the same thing — and your redirect never executes.
The PHP reflex
Here is how you guard a page in Symfony:
public function login(): Response
{
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('dashboard');
}
// render the login form
}
redirectToRoute() returns a RedirectResponse. The return keyword is load-bearing — the method returns a response object, the controller exits implicitly, and the framework sends the redirect. You can put this inside a try/catch without issue because nothing throws.
The same reflex in Laravel:
if (Auth::check()) {
return redirect('/dashboard');
}
Same pattern. redirect() returns a RedirectResponse. The return does the work. Try/catch around it changes nothing.
The Next.js difference
Now consider this guard in a Next.js layout, written by the same developer who wrote the Symfony version above:
try {
decodeToken(jwt) // ← might throw if the token is malformed
redirect('/dashboard') // ← should navigate away — but doesn't
} catch {
// token was invalid, stay on this page
}
The code reads correctly: try to decode the token, if it works redirect to the dashboard, if it fails catch the error and stay put. A PHP developer would write this without a second thought — it's the exact same structure as a Symfony controller guard, just in TypeScript.
Here is what actually happens:
decodeToken(jwt)succeeds.redirect('/dashboard')is called.redirect()throwsNEXT_REDIRECT— it never returns.- The
catchblock catchesNEXT_REDIRECTjust like any other error. - The catch block does nothing (the token was valid — no error to handle).
- Execution continues past the
ifblock. - The login form renders. The user stays on
/login.
No console error. No log. The redirect simply never happens, because the framework's redirect mechanism (throwing an error) was intercepted by userland code before the framework could see it.
The fix is to separate the fallible operation from the redirect:
let isValid = true
try {
decodeToken(jwt)
} catch {
isValid = false
}
if (isValid) {
redirect('/dashboard') // ← outside try/catch — framework sees it
}
Two worlds, one language
The difference is not in the syntax — both PHP and TypeScript use try, catch, and throw. The difference is in what a redirect is:
| PHP (Symfony/Laravel) | Next.js | |
|---|---|---|
| Redirect type | Return value (Response) | Exception (NEXT_REDIRECT) |
| Return type | Response (explicit) | never (never returns) |
| In try/catch | Safe — no effect | Breaks — caught as error |
| Framework boundary | return keyword | Framework-level catch |
Writing the "wrong" version above is not a JavaScript mistake. It is applying a correct mental model from one stack to another — and the new stack has a fundamentally different contract for the same operation.
Why this matters beyond one bug
The redirect() gotcha is a specific instance of a broader pattern: the PHP web framework model is built around returning responses, where a return value is the unit of control flow. Next.js and the React server component model are built around throwing responses (redirects, not-found, error boundaries), where control flow is mediated by the framework catching special exceptions at rendering boundaries.
The two models produce code that looks the same on the surface. They even use the same keywords — try, return, throw. But they have opposite semantics for one critical operation. That is the kind of difference that is invisible in a tutorial and discoverable only by making the mistake.
Delaa