Mesrai
Back to blog
// essayTechnical Deep Dive

Next.js redirect() Inside Render: The Suspense Boundary Trap

Real PR catch: `redirect()` inside JSX conditional after data fetch — interrupts render, breaks Suspense. Top-of-component placement fix.

Mesrai TeamAugust 9, 20268 min read

A protected page calling `redirect()` if the user is missing. Author placed it after data fetching and inside conditional JSX. Next.js threw on every render — `redirect` interrupts the render cycle.

The vulnerable diff

tsxapp/dashboard/page.tsx
// app/dashboard/page.tsx
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const user = await getCurrentUser();
  const data = await fetchDashboardData(user?.id);  // crashes if no user
  return (
    <div>
      {!user && redirect("/login")}  {/* BUG: in render */}
      <Stats data={data} />
    </div>
  );
}

What is wrong

Next.js's `redirect()` works by throwing a special error that the framework catches and converts to a 307. Called inside render — particularly after `await`s have started — the throw interrupts the rendering process, breaks Suspense boundaries, sometimes triggers an infinite render loop if not handled. The correct pattern is to call `redirect()` early, before any other work, at the top of the server component.

The attack

Repro: visit /dashboard while logged out. Either you see an error boundary or the page reloads in a loop.

textexploit
# Logs:
Error: NEXT_REDIRECT — caught by error boundary instead of framework.
The redirect did not fire; the boundary rendered "Something went wrong".

Sometimes the redirect does happen but only after the error boundary triggers a re-render, leading to flashing UI.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Language] [Next-Redirect] [medium]

`redirect()` works by throwing. Call it at the top of the server
component, before any awaited work or JSX. Two problems with the
current placement:

  1. fetchDashboardData runs even when there is no user — wastes
     the call, might 500 because user.id is undefined.

  2. The redirect inside the JSX expression executes during render,
     which interrupts the render tree and can break Suspense.

Fix shape:

  export default async function Dashboard() {
    const user = await getCurrentUser();
    if (!user) redirect("/login");
    // user is non-null past this point
    const data = await fetchDashboardData(user.id);
    return <Stats data={data} />;
  }

If you need authorization (not just authentication), do that early too:

  if (!user) redirect("/login");
  if (!user.canViewDashboard) redirect("/no-access");

The fix

tsxapp/dashboard/page.tsx (fixed)
// app/dashboard/page.tsx — fixed
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const user = await getCurrentUser();
  if (!user) redirect("/login");
  // user is non-null after this guard

  const data = await fetchDashboardData(user.id);
  return <Stats data={data} />;
}

Redirect is the first guard after auth fetch. Data fetch only happens for logged-in users. The user is non-null after the guard so TypeScript can narrow correctly. No conditional inside JSX, no awaits between the auth check and the redirect.

Why human review missed it

Next.js's redirect-as-throw pattern is non-obvious. The error message when it misfires is unclear. The bug shows up only in specific code paths (logged-out users on a logged-in page). Mesrai catches `redirect()` calls inside JSX or after non-trivial awaits, and recommends moving them to the top of the server component.

Related rules + further reading

Mesrai rule pack: language/next-redirect-placement — flags redirect() inside JSX or after data fetches.

Next.js docs: redirect() in Server Components.

Common pattern as teams migrate from useEffect-based redirects to server-side.

Takeaway

Redirect early. Before awaits, before JSX. Throws clean. Mesrai catches the misplaced pattern.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.