Type something to search...
Next.js  forbidden.js

Next.js forbidden.js

Most authorization failures in a web app fall into one of two buckets, and they deserve different responses: "you're not logged in" (401, please sign in) and "you're logged in, but you're not allowed to see this" (403, this isn't for you). Next.js has had notFound() and not-found.js for a long time to hide the existence of a resource entirely. forbidden.js is the newer, narrower counterpart — a special file that renders when your code explicitly calls the forbidden() function during an authorization check, and it correctly returns a 403 status code rather than reusing 404 for a fundamentally different situation.

This is a short reference by design — the file itself has almost no surface area — but the distinction it represents (403 vs. 404 as a genuine user-facing signal, not just an HTTP status code nobody reads) is worth understanding properly.

Basic Usage

import Link from "next/link";

export default function Forbidden() {
  return (
    <div>
      <h2>Forbidden</h2>
      <p>You are not authorized to access this resource.</p>
      <Link href="/">Return Home</Link>
    </div>
  );
}

Placed in the app directory (or scoped to a specific segment, following the same nesting rules as not-found.js and error.js), this file is what renders whenever your authentication or authorization logic calls the paired forbidden() function from next/navigation. You don't invoke forbidden.js directly — it's the UI half of a pair, where forbidden() is the imperative trigger and forbidden.js is the resulting fallback screen.

Why 403 and Not 404

It's tempting to just reuse notFound() for every access-denial case, since it's the tool most people reach for first and it already exists in every Next.js app. But 404 and 403 communicate genuinely different things, both to a human and to any tooling (crawlers, monitoring, security scanners) reading the response:

  • 404 (Not Found) — as far as the requester should know, nothing exists at this URL. This is the right choice when you don't want to confirm or deny that a resource even exists to someone who isn't authorized to know that — a genuinely useful privacy property in some systems.
  • 403 (Forbidden) — the resource exists, and you know exactly what's there, but this particular request isn't permitted to access it. This is the honest, transparent answer for the far more common case: a logged-in user hitting an admin page they don't have the role for, or a paid feature behind a subscription tier they haven't purchased.

Silently 404-ing every authorization failure trains your users (and your own debugging instincts) to distrust every not-found page — was the thing actually deleted, or was it just permissions? forbidden.js exists so you don't have to make that tradeoff by default; you can be explicit when explicitness is the right call, and reserve 404 for situations where obscuring existence is actually the point.

Props

forbidden.js components accept no props at all. There's no error object, no params, nothing to destructure — it's a pure, static fallback component. If your forbidden page needs to know why access was denied (missing role, expired subscription, IP-restricted region), that logic has to live in whatever code called forbidden() in the first place, and be communicated to the user through some other mechanism — a query param your fallback reads via searchParams if it's a page-level page.js, or a value your authorization layer writes somewhere the UI can independently look up.

import { forbidden } from "next/navigation";
import { getCurrentUser } from "@/lib/auth";

export default async function AdminPage() {
  const user = await getCurrentUser();

  if (user.role !== "admin") {
    forbidden();
  }

  return <AdminDashboard />;
}

Calling forbidden() here interrupts rendering of AdminPage and causes the nearest forbidden.js boundary — following the same segment-nesting rules as error.js and not-found.js — to render instead, with a 403 status code attached to the response.

Experimental Status

Worth flagging clearly, since it changes how you should treat this in production planning: as of this writing, forbidden.js (and its paired forbidden() function) is still marked experimental in the official docs, with an explicit note that it's not recommended for production use yet. If you're building something today, that means:

  • Expect the API surface (function signature, file behavior, or even the convention's existence) to potentially change in a future release without the usual deprecation runway a stable API would get.
  • If you need reliable 403 handling in production right now, the more conservative path is to implement it yourself — a custom Route Handler middleware, or simply rendering a 403-styled response directly from within your page/layout logic without relying on this specific convention — and revisit adopting forbidden.js once it graduates to stable.
  • If you do experiment with it, the Next.js team explicitly invites feedback via GitHub issues — this is exactly the kind of feature where early real-world usage reports shape what ships as stable.

Version History

VersionChanges
v15.1.0forbidden.js introduced

Key Takeaways

AspectDetail
PurposeRenders when forbidden() is called; returns a 403 status
PropsNone — a static component with no error, params, or other data
vs. not-found.jsUse 403 when you want to be honest that the resource exists but isn't permitted; use 404 to obscure existence entirely
StatusExperimental — not recommended for production as of this writing
Where to check for updatesOfficial Next.js GitHub issues track feedback on this feature's stabilization

The whole point of forbidden.js is to give your app a clean, correct way to say "no" without pretending something doesn't exist when it does. It's a small file with an outsized effect on how trustworthy your error states feel to real users — just build it with the awareness that it's still evolving, and plan your production authorization flow accordingly until it stabilizes.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper