Skip to the solution
MediumFrontend8 min read

What are forbidden() and unauthorized() in Next.js?

81 views

The problem

Question presented to candidate: "Our admin pages redirect everyone without access to /login, even users who are signed in but are not admins. How would you return proper 401 and 403 responses in the App Router, and what are forbidden() and unauthorized()?"

What a strong answer should cover:

  • 401 means "we do not know who you are" (sign in); 403 means "we know you, and the answer is no". unauthorized() and forbidden() from next/navigation throw and render unauthorized.tsx / forbidden.tsx, like notFound() does for 404.
  • They are still experimental in 16.4.2: the config option experimental.authInterrupts: true is required; without it the call throws an error at runtime.
  • They work in Server Components, Server Actions and Route Handlers; they cannot be called in the root layout; both add a noindex robots tag.
  • Status codes: the real 401/403 is sent only if the call happens before streaming starts; inside a <Suspense> boundary the status stays 200 and the fallback UI streams in.
  • They throw, so a try/catch swallows them unless you use unstable_rethrow; put the checks in a Data Access Layer.

Clarifying questions expected:

  • "Should a signed-out user see a sign-in page or be redirected?" β€” unauthorized() renders UI in place with a 401; redirect() sends them away.
  • "Do crawlers or API clients depend on the status code?" β€” then the check must run before any streaming.

Code / implementation expected: Yes β€” requireUser/requireAdmin helpers, the two special files, and the authInterrupts config.

  • nextjs
  • auth
  • errors
  • file-conventions

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 8 sections

Read the code

Config, DAL helpers and the 403/401 files
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    authInterrupts: true, // required for forbidden() and unauthorized() in 16.4.2
  },
};
export default nextConfig;

// lib/dal.ts
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";
import { forbidden, unauthorized } from "next/navigation";

export type Session = { userId: string; role: "user" | "admin" };

// Lab stand-in for a real session lookup (decrypt a cookie, query a sessions table, ...).
export const getSession = cache(async (): Promise<Session | null> => {
  const role = (await cookies()).get("role")?.value;
  if (role === "admin" || role === "user") return { userId: "u1", role };
  return null;
});

export async function requireUser() {
  const session = await getSession();
  if (!session) unauthorized(); // 401: we do not know who you are
  return session;
}

export async function requireAdmin() {
  const session = await requireUser();
  if (session.role !== "admin") forbidden(); // 403: we know you, and the answer is no
  return session;
}

// app/forbidden.tsx
import Link from "next/link";
export default function Forbidden() {
  return (
    <main>
      <h2>403 - You do not have access</h2>
      <p>Ask an admin for the admin role.</p>
      <Link href="/">Back home</Link>
    </main>
  );
}

// app/unauthorized.tsx
import Link from "next/link";
export default function Unauthorized() {
  return (
    <main>
      <h2>401 - Please sign in</h2>
      <Link href="/login">Sign in</Link>
    </main>
  );
}

// app/admin/page.tsx
import { requireAdmin } from "@/lib/dal";

export default async function AdminPage() {
  const session = await requireAdmin();
  return <h1>Admin dashboard for {session.userId}</h1>;
}
try/catch swallows the interrupt; unstable_rethrow fixes it
// app/rethrow/page.tsx
import { unstable_rethrow } from "next/navigation";
import { requireAdmin } from "@/lib/dal";

export default async function Page() {
  try {
    await requireAdmin();
  } catch (e) {
    unstable_rethrow(e); // lets Next.js interrupts (403/401/404/redirect) through
    return <h1>Something went wrong</h1>;
  }
  return <h1>Admin only</h1>;
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 94 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextMedium

How does Next.js handle CSS (CSS Modules, global, Tailwind, CSS-in-JS)?

Continue