unauthorized.js
Renders 401 UI when unauthorized() is thrown, typically a login prompt for an absent session.
export default function Unauthorized(): React.ReactNode- Added in Next.js v15.1.0
Return value
React.ReactNode | Promise<React.ReactNode>The UI rendered in place of the segment when unauthorized() is thrown; Next.js returns a 401 status for non-streamed responses.
Errors
Nothing itself, but the unauthorized() call that triggers it throws and is swallowed by any try/catch around it; use unstable_rethrow to let the interrupt through.
Changelog
- PHP v15.1.0unauthorized.js introduced. Still experimental in v16 and not recommended for production.
- PHP v15.1.0Requires experimental.authInterrupts in next.config.js, the same flag that enables forbidden().
The unauthorized file renders when unauthorized() is invoked, and Next.js returns a 401. It is the "you are not signed in" case, as distinct from forbidden.js's "you are signed in but not allowed". Both are experimental and share one switch:
// next.config.ts
const nextConfig = { experimental: { authInterrupts: true } }
Its real advantage over a redirect to /login is that the user stays on the URL they asked for. Render the login form inline and the session, once created, drops them straight into the page — no ?returnTo= round trip, no lost deep link, and the status code stays honest for API clients.
What trips people up
- Nothing happens without
authInterrupts. try/catchsuppresses it. The call throws; a catch block around your session check silently discards the interrupt.- It takes no props, so the login component it renders must work out where to return to on its own — usually from the pathname, read in a Client Component.
- Streaming fixes the status at 200 if the check runs inside a Suspense boundary.
- It is not a substitute for checks in Server Functions. Verify the session in every Server Function too; a proxy matcher will not cover them reliably.
Example
import Login from '@/app/components/Login'
export default function Unauthorized() {
return (
<main>
<h1>401 - Unauthorized</h1>
<p>Please log in to access this page.</p>
<Login />
</main>
)
}When to use it
- Render a login form in place while keeping the user on the deep link they requested
- Return a real 401 from a dashboard route when the session cookie is missing or expired
- Separate the signed-out case from the wrong-role case for clearer analytics
- Prompt for re-authentication after a session expires mid-visit
Frequently asked questions
Why is unauthorized() not working in Next.js?
Either experimental.authInterrupts is not enabled in next.config.ts — the function is unavailable without it — or a try/catch around your session check is swallowing the interrupt, since unauthorized() works by throwing. Enable the flag and restart the dev server, and use unstable_rethrow inside any catch block that wraps the call.
Should I use unauthorized.js or redirect to a login page?
unauthorized.js keeps the user on the URL they requested and returns a genuine 401, so a deep link survives sign-in without a returnTo query parameter and API clients see the correct status. A redirect to /login is the stable, non-experimental option and is still the safer choice for production, since authInterrupts may change between minor releases.
Is unauthorized.js enough to secure a route?
No. It renders UI for a check you wrote; it does not perform one. Verify the session inside every Server Component and Server Function that touches protected data, since Server Functions are POST requests to their host route and a proxy matcher can miss them. Treat proxy.js checks as an optimization, not the security boundary.
More examples
Guarding a dashboard page
With no session, unauthorized() throws and the nearest unauthorized.js renders in place of the dashboard while the URL stays /dashboard. Because it throws, execution never reaches the return and TypeScript narrows session to non-null below the guard — there is no need to write return unauthorized().
import { verifySession } from '@/app/lib/dal'
import { unauthorized } from 'next/navigation'
export default async function DashboardPage() {
const session = await verifySession()
if (!session) {
unauthorized()
}
return <div>Dashboard</div>
}Enabling the interrupts
This is next.config.ts. The single authInterrupts flag turns on both unauthorized() and forbidden(); without it neither function is available and both files sit unused. Restart the dev server after changing it — config changes are not hot-reloaded.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
}
export default nextConfigNot a replacement for checks inside Server Functions
Server Functions are POST requests to the route that uses them, so a proxy matcher excluding that path silently skips them — and moving the function to another route can quietly drop coverage. Verifying the session inside the function keeps the check attached to the mutation rather than to a URL pattern, and scoping the delete by ownerId stops one authenticated user acting on another's data.
'use server'
import { verifySession } from '@/app/lib/dal'
import { unauthorized } from 'next/navigation'
export async function deleteProject(id: string) {
const session = await verifySession()
if (!session) {
unauthorized()
}
await db.projects.delete({ where: { id, ownerId: session.userId } })
}
Discussion