unauthorized()
Throws a 401 interrupt rendering the nearest unauthorized.js boundary when a request is not signed in.
unauthorized(): never- Added in Next.js v15.1.0
Return value
neverDoes not return a value. It throws a NEXT_HTTP_ERROR_FALLBACK;401 error and terminates rendering of the route segment where it was called.
Errors
Always throws NEXT_HTTP_ERROR_FALLBACK;401 by design; it also throws if the experimental authInterrupts flag is not enabled, and it cannot be called in the root layout.
Changelog
- PHP v15.1.0unauthorized introduced behind the experimental authInterrupts flag.
unauthorized() is for requests that are not signed in — no session, an expired token, a missing cookie. It renders the nearest unauthorized.js, which is where a login form usually lives. If the user is signed in but lacks permission, that is 403 territory: use forbidden().
It is experimental and requires experimental.authInterrupts: true in next.config.ts.
The call throws NEXT_HTTP_ERROR_FALLBACK;401, unwinds the segment, and injects <meta name="robots" content="noindex" />. It cannot be called in the root layout.
Choosing this over redirect()
Redirecting anonymous visitors to /login loses the URL they wanted and adds a navigation. unauthorized() keeps them on the same URL and renders the login UI in place, so signing in returns them to what they asked for. The trade-off is the same as its siblings: once the response has started streaming — from a <Suspense> boundary, or a dynamic route's static shell — the status is already 200. Check in proxy when the real 401 matters, for example for API consumers.
Example
import { unauthorized } from 'next/navigation'
import { verifySession } from '@/app/lib/dal'
export default async function DashboardPage() {
const session = await verifySession()
if (!session) {
unauthorized()
}
return (
<main>
<h1>Welcome to the Dashboard</h1>
<p>Hi, {session.user.name}.</p>
</main>
)
}When to use it
- Render an in-place login form when a dashboard request has no session
- Reject an anonymous caller inside a Server Function before it mutates anything
- Return 401 from a Route Handler when the session cookie is missing or expired
- Gate a data-access function so every component awaiting it inherits the session check
Frequently asked questions
What is the difference between unauthorized and forbidden in Next.js?
unauthorized() means 401, no valid session — the caller should sign in. forbidden() means 403, the caller is authenticated but not permitted. Both need the experimental authInterrupts flag and render their own file convention, unauthorized.js and forbidden.js.
Should I use unauthorized() or redirect('/login')?
unauthorized() keeps the visitor on the requested URL and renders the login UI in place, so no return-to parameter is needed. redirect('/login') changes the URL and is the right choice when your login page carries its own flow, such as an external identity provider.
Why is unauthorized() not defined?
The experimental authInterrupts option is not enabled in next.config. Set experimental: { authInterrupts: true } and restart the dev server. The function was added in v15.1.0, so older versions do not export it at all.
More examples
The unauthorized.tsx boundary with a login form
Because the boundary replaces the segment in place, the browser URL stays on /dashboard rather than moving to /login. After a successful sign-in the user is already at the destination they asked for, with no return-to parameter to thread through.
// app/unauthorized.tsx
import Login from '@/app/components/Login'
export default function UnauthorizedPage() {
return (
<main>
<h1>401 - Unauthorized</h1>
<p>Please log in to access this page.</p>
<Login />
</main>
)
}Session check in the Data Access Layer
The Account heading and the loading fallback reach the browser before the session resolves. When verifySession returns nothing, the throw propagates out of the Suspense boundary and unauthorized.tsx renders in place of the details. The response headers were already sent as 200, so this is a soft 401.
// app/account/page.tsx
import { Suspense } from 'react'
import { unauthorized } from 'next/navigation'
import { verifySession } from '@/app/lib/dal'
import { db } from '@/app/lib/db'
async function getAccount() {
const session = await verifySession()
if (!session) {
unauthorized()
}
return db.accounts.findByUserId(session.userId)
}
async function AccountDetails() {
const account = await getAccount()
return <p>Signed in as {account.email}</p>
}
export default function AccountPage() {
return (
<main>
<h1>Account</h1>
<Suspense fallback={<p>Loading...</p>}>
<AccountDetails />
</Suspense>
</main>
)
}Guarding a Route Handler
Nothing is streaming inside a Route Handler at the point of the call, so the caller receives a genuine 401 rather than a soft one. The db query below is unreachable for an anonymous request because the call throws first — no return statement is needed.
// app/api/profile/route.ts
import { NextResponse } from 'next/server'
import { unauthorized } from 'next/navigation'
import { verifySession } from '@/app/lib/dal'
import { db } from '@/app/lib/db'
export async function GET() {
const session = await verifySession()
if (!session) {
unauthorized()
}
const profile = await db.profile.findByUserId(session.userId)
return NextResponse.json(profile)
}
Discussion