Build1 publisher3 min readPublished Updated
A try/catch around a Next.js server action swallows the redirect it was meant to protect
redirect() in Next.js works by throwing an internal error that the framework catches upstream to trigger navigation, so any catch block wide enough to enclose it turns a successful login into a logged failure.
The Engineer · Build desk

What happened
- A try/catch wrapped around the whole server action intercepts that signal like any other exception, so a successful login returns the generic error object and never navigates anywhere.
- The dev.to post's preferred fix narrows the try block to the credential verification and calls redirect() after it, so the internal throw reaches the framework uninterrupted.
- notFound() signals through the same throw, putting any Server Action, Server Component or route handler with a broad catch at risk of the identical silent failure.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Where a redirect sits inside an action, the catch-all wrapper is off the table: the position of redirect() now dictates where error handling is allowed to go.
- decision Teams that keep the broad try are signing off on a deep import from Next's compiled output to tell control flow from failure, or they rewrite the action.
- precedent Any framework API that signals by throwing inherits this conflict with application catch blocks, and notFound() is already the second case in the same post.
JavaScript gives a function one way to leave without returning, and Next.js uses it. Calling `redirect('/dashboard')` throws. The thrown value travels up the stack until something catches it, and the framework's own machinery catches it higher in the rendering or request lifecycle and converts it into the navigation [1]. The dev.to post that walks through this says the throw is deliberate, not a bug [1]. The language does not mark it as privileged, so the nearest enclosing catch wins, whether it belongs to the framework or to you [2].
In the login action the post uses as its example, the whole body sits inside one try, so the catch logs "Login failed:" and returns `{ error: 'Something went wrong' }` after the credentials verified and the session cookie was written [3]. The collision comes from the framework putting a control-flow signal on the same channel the application's error handling owns [1][2]. Nothing in the wrapped version misuses try/catch. The post calls it code that "looks like exactly the kind of defensive coding you'd want" [14].
Notice which path fires. A bad password returns `{ error: 'Invalid credentials' }` from the `!user` check before `redirect()` is ever reached [6], so the failure path behaves as written. The swallow lands only on success: every correct login, none of the rejected ones [13]. A test that exercises the wrong-password branch will pass.
The console does not help either. The logged error carries a message or code from Next.js's internal redirect machinery rather than a shape a JavaScript developer recognises [5]. The post wrote that the "error" being caught is, ironically, "the successful outcome trying to happen" [12].
The fix the post prefers narrows the try to `verifyCredentials` and calls `redirect()` after the block, where its throw reaches Next.js cleanly [7]. I would default to that as well. Read the rewritten action before copying it: `setSessionCookie(user)` now sits outside the try, after the `!user` check [8]. A cookie write that throws propagates instead of returning the generic error object. If that call is genuinely risky, it needs a try of its own.
When the structure needs the broad try, the post's alternative catches everything and re-throws whenever `isRedirectError(error)` identifies the value as a redirect signal [9]. That guard imports from `next/dist/client/components/redirect`, a path into Next's compiled output [9]. The post calls the pattern more fragile because it depends on an internal Next.js utility instead of a stable public API, and recommends restructuring wherever the code allows [10].
`notFound()` throws through the same route [11]. Any Server Action, Server Component or route handler whose broad catch encloses either call is exposed to the same silent failure [11].
What to watch
- Whether Next.js ships a supported way to distinguish framework control-flow throws from real errors, so the guard does not need a next/dist import.
- Whether a future release moves or renames the internal redirect module, breaking the re-throw guard in code that currently works.
- Whether lint rules start flagging redirect() and notFound() calls that sit inside a try block.