unauthorized.js

Renders 401 UI when unauthorized() is thrown, typically a login prompt for an absent session.

Signature
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/catch suppresses 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

Example · typescript
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().

Example · typescript
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.

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    authInterrupts: true,
  },
}

export default nextConfig

Not 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.

Example · typescript
'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

  • Be the first to comment on this lesson.