unauthorized() and forbidden() in Next.js 16: Auth Interrupts for Clean Access Control
Next.js 16 adds unauthorized() and forbidden() — two functions that terminate rendering with a proper 401 or 403 and render a custom page. Here's how to use them in Server Components, Server Functions, and AI route handlers.
5 min read
Before Next.js 15.1, returning a 401 or 403 from a Server Component required either throwing a custom error and catching it in an error boundary or redirecting the user manually. Neither approach gave you a clean HTTP status code on the initial request. The new unauthorized() and forbidden() functions — collectively called auth interrupts — fix that. They terminate rendering immediately, emit the correct HTTP status, and render a dedicated page you control.
Enabling auth interrupts
Auth interrupts are opt-in behind an experimental flag. Add one line to your config:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
export default nextConfig;Once enabled, you can call unauthorized() and forbidden() from Server Components, Server Functions, and Route Handlers. The root layout is the one place they cannot be called — Next.js renders the root layout before the auth check can take effect.
unauthorized() — unauthenticated requests
Call unauthorized() when the request carries no valid session. Next.js stops rendering, sends an HTTP 401, and renders app/unauthorized.tsx in its place:
// app/dashboard/page.tsx
import { verifySession } from "@/lib/session";
import { unauthorized } from "next/navigation";
export default async function DashboardPage() {
const session = await verifySession();
if (!session) {
unauthorized();
}
return <div>Welcome, {session.user.name}</div>;
}// app/unauthorized.tsx
import Link from "next/link";
export default function UnauthorizedPage() {
return (
<main>
<h1>Sign in required</h1>
<p>You need to be signed in to view this page.</p>
<Link href="/login">Sign in</Link>
</main>
);
}The unauthorized.tsx file co-locates with the routes it protects, just like error.tsx and not-found.tsx. A root-level one covers the whole app; segment-level files can override it for specific routes.
forbidden() — insufficient permissions
Call forbidden() when the session exists but the user lacks the required role or scope. It sends HTTP 403 and renders app/forbidden.tsx:
// app/admin/page.tsx
import { verifySession } from "@/lib/session";
import { forbidden } from "next/navigation";
export default async function AdminPage() {
const session = await verifySession();
if (!session) {
unauthorized();
}
if (session.role !== "admin") {
forbidden();
}
return <div>Admin Dashboard</div>;
}Keeping unauthorized and forbiddenseparate matters for clients that act on status codes programmatically — a 401 should prompt a re-authentication flow; a 403 should surface a “you don’t have access” message, not a login form.
Using auth interrupts in Server Functions
AI-native apps commonly gate expensive LLM calls behind auth checks in Server Functions. Auth interrupts work there too:
// app/actions/generate.ts
"use server";
import { verifySession } from "@/lib/session";
import { unauthorized, forbidden } from "next/navigation";
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
export async function generateReport(prompt: string) {
const session = await verifySession();
if (!session) {
unauthorized();
}
if (!session.plan.includes("pro")) {
forbidden();
}
const { text } = await generateText({
model: anthropic("claude-opus-5"),
prompt,
});
return text;
}The function never reaches the LLM call unless the session is valid and the user is on the right plan. No try/catch required — the interrupts throw internally and Next.js handles the rest.
What stays the same
Auth interrupts complement redirect() — they do not replace it. Use unauthorized()/forbidden() when the correct response is a status code and a descriptive page. Use redirect() when you want to send the user somewhere else entirely, such as a login flow with a callbackUrl parameter.
For middleware-level auth (blocking unauthenticated requests before any Server Component runs), the existing middleware.ts + NextResponse.redirect() pattern is still the right tool. Auth interrupts are for component-level decisions where you need the full session context before you know which interrupt to trigger.
The practical upside
Auth interrupts make per-component access control as concise as notFound(). For AI products where different routes, Server Functions, and API handlers have different entitlement requirements, being able to express each guard in two lines — verify, interrupt — keeps authorization logic obvious and co-located with the code it protects.