
Next.js unauthorized.js
unauthorized.js completes a trio of authorization-adjacent special files alongside forbidden.js and the long-standing not-found.js — and of the three, it's the one that maps most directly onto a scenario every app with a login flow eventually has to handle: someone visiting a page that requires being signed in, while they aren't. Where forbidden.js says "you're logged in, but this still isn't for you," unauthorized.js says the more fundamental "you're not logged in at all, and this page requires it" — a genuinely distinct condition, with its own correct HTTP status code and its own dedicated file convention.
Basic Usage
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>
);
}
This renders whenever your code calls the paired unauthorized() function (from next/navigation) inside a route segment, and Next.js returns a 401 status code alongside it — the correct HTTP status for "authentication is required and has not been provided," distinct from both 404 (nothing here) and 403 (something's here, but you can't have it regardless of who you are).
401 vs. 403 vs. 404 — Getting the Semantics Right
It's worth being precise about this distinction, since conflating any two of these status codes genuinely confuses both users and any tooling reading your responses:
- 401 (Unauthorized) — the visitor isn't authenticated at all. The fix is to log in; access might well be granted immediately afterward.
- 403 (Forbidden) — the visitor is authenticated, but their identity doesn't have permission for this specific resource. Logging in again wouldn't help — a different account, or a different permission level, would.
- 404 (Not Found) — you're deliberately not confirming the resource exists at all, to anyone who isn't authorized to know that.
unauthorized.js is specifically for the first case. Reaching for forbidden() or notFound() when what you actually mean is "please log in" sends a status code (and a message) that doesn't match the real situation, and can send genuinely confused users down the wrong troubleshooting path — retrying, refreshing, assuming it's a bug — when the actual fix is simply signing in.
Props
Like forbidden.js, unauthorized.js components accept no props at all — a pure, static fallback component. Any context about why access was denied has to be communicated through some other channel your own code controls, not through anything this file receives automatically.
Example: Gating a Page and Showing a Login UI
The canonical pattern pairs a session check inside a page with the unauthorized() function, and a matching unauthorized.js that renders the actual login experience:
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>;
}
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>
);
}
Calling verifySession() inside the Data Access Layer (rather than trusting a client-passed flag) and checking its result directly in the page is the pattern worth internalizing here, independent of this specific file — unauthorized() is only as trustworthy as the session check that decides to call it. The file itself is just the UI half of the pair; the actual security boundary lives in verifySession and wherever else your app validates that a session is genuinely real.
Experimental Status — Read Before Shipping This
Exactly like forbidden.js, this convention is explicitly marked experimental and not recommended for production as of this writing. That has concrete implications for how you should plan around it:
- The function signature, the file's behavior, or its continued existence could change in a future release without the deprecation runway a stable API would get.
- If you need reliable 401 handling in production today, building it yourself — checking session state and rendering a 401-styled response directly, without relying on this specific convention — is the more conservative choice until it stabilizes.
- The Next.js team explicitly solicits feedback on this feature via GitHub issues, which is the right channel if you do experiment with it and hit rough edges worth reporting.
Version History
| Version | Changes |
|---|---|
v15.1.0 | unauthorized.js introduced |
Key Takeaways
| Aspect | Detail |
|---|---|
| Triggered by | The unauthorized() function, called from next/navigation |
| Status code | 401 |
| Props | None — a static component |
vs. forbidden.js | Use 401 when the visitor isn't authenticated at all; use 403 when they are, but still lack permission |
| Typical pairing | A session check (via your Data Access Layer) that calls unauthorized() on failure |
| Status | Experimental — not recommended for production as of this writing |
The value of having a dedicated unauthorized.js rather than reusing notFound() or a generic error page for every access-denial scenario is precision: users (and any monitoring reading your response codes) get an honest, specific signal about exactly what went wrong and what would actually fix it. Just build with the awareness that this particular convention is still evolving, and plan your production authentication flow with that in mind until it stabilizes.


