DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Wrapping redirect() in a try/catch in Next.js Can Silently Swallow the Redirect

This one catches people specifically because the code looks like careful error handling, and it's actually the thing breaking the exact feature it's wrapped around.

The Setup That Looks Like Responsible Error Handling

// actions/auth.ts
'use server';

export async function login(formData: FormData) {
  try {
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;

    const user = await verifyCredentials(email, password);
    if (!user) {
      return { error: 'Invalid credentials' };
    }

    await setSessionCookie(user);
    redirect('/dashboard');
  } catch (error) {
    console.error('Login failed:', error);
    return { error: 'Something went wrong' };
  }
}
Enter fullscreen mode Exit fullscreen mode

This looks like exactly the kind of defensive coding you'd want, wrap the risky operations, catch anything unexpected, return a clean error instead of crashing. It also means a successful login never actually redirects anywhere, and the console gets a confusing, misleading error log every single time someone logs in correctly.

Why This Happens

redirect() in Next.js doesn't work like a normal function returning a value that tells the framework "please navigate now." It works by throwing a special internal error, effectively an escape hatch, that Next.js's own machinery catches at a higher level in the rendering or request lifecycle, and uses to actually trigger the navigation. This is genuinely how the feature is implemented under the hood, not a bug, it's a deliberate mechanism for interrupting normal execution flow to redirect.

A try/catch block doesn't know or care that this particular thrown error is special. It catches everything thrown within its boundary, indiscriminately, which includes this internal redirect signal exactly the same way it would catch a genuine database error or a network failure. Your catch block intercepts it, logs a confusing "Login failed" message for what was actually a completely successful login, and returns your generic error response instead of ever letting the redirect signal propagate up to where Next.js needs to see it.

Why the Console Log Makes This Extra Confusing

The logged error typically has a distinctive-looking message or code related to Next.js's internal redirect mechanism, not a normal JavaScript error you'd immediately recognize. Someone unfamiliar with this specific gotcha sees an unfamiliar-looking error logged on every successful login and reasonably assumes something is actually broken, when the "error" being caught is, ironically, the successful outcome trying to happen.

The Actual Fix: Call redirect() Outside the try/catch

// actions/auth.ts
'use server';

export async function login(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  let user;
  try {
    user = await verifyCredentials(email, password);
  } catch (error) {
    console.error('Login failed:', error);
    return { error: 'Something went wrong' };
  }

  if (!user) {
    return { error: 'Invalid credentials' };
  }

  await setSessionCookie(user);
  redirect('/dashboard'); // outside any try/catch, free to throw its internal signal cleanly
}
Enter fullscreen mode Exit fullscreen mode

Scoping the try/catch specifically around the operation that can genuinely fail in an unexpected way, the credential verification, database call, whatever's actually risky, and calling redirect() afterward, outside that block entirely, means its internal throw mechanism reaches Next.js cleanly, without your own error handling accidentally standing in the way.

If You Need Try/Catch Around Code That Includes a redirect() Call

Sometimes the structure genuinely requires a broader try block. In that case, explicitly re-throw anything that looks like a Next.js redirect or notFound signal, rather than treating everything caught as a genuine error:

import { isRedirectError } from 'next/dist/client/components/redirect';

try {
  // some code that includes a redirect() call somewhere within it
} catch (error) {
  if (isRedirectError(error)) {
    throw error; // let it propagate, this isn't a real error
  }
  console.error('Actual error:', error);
  return { error: 'Something went wrong' };
}
Enter fullscreen mode Exit fullscreen mode

This pattern is more defensive but also more fragile, since it depends on an internal Next.js utility rather than a fully public, stable API. Restructuring the code to keep redirect() outside any try/catch entirely, the first fix above, is the cleaner, more robust solution whenever the code can reasonably be organized that way.

Where Else This Bites

The exact same issue applies to notFound(), which uses the same underlying throw-based mechanism. Any Server Action, Server Component, or route handler with a broad try/catch wrapping code that also calls redirect() or notFound() anywhere within that boundary is at risk of this exact silent failure, not just login flows specifically.

The Actual Rule

Never let a try/catch block unintentionally wrap a call to redirect() or notFound(). Scope error handling tightly around the specific operations that can genuinely throw a real error, and keep redirect and not-found calls outside that boundary, or explicitly detect and re-throw them if the structure genuinely requires a broader catch.


If you've got a Server Action with a broad try/catch that also calls redirect() somewhere inside it, go check whether that redirect is actually firing, or silently getting logged as a mysterious error instead. Drop what you find in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)