Introduction
When a user doesn't have permission, they shouldn't just see a generic error. A well-designed forbidden page explains what happened and gives them a path forward—whether that's requesting access, contacting support, or logging in with a different account.
Key Concepts
Custom error pages in Next.js handle specific HTTP error scenarios:
not-found.tsx- 404 errorsforbidden.tsx- 403 errors (Next.js 15+)unauthorized.tsx- 401 errors (Next.js 15+)
Real World Context
Good error pages:
- Reduce support tickets ("Why can't I access this?")
- Improve security by not leaking information
- Guide users to the right action
- Maintain your app's branding and UX
Deep Dive
Forbidden Page (Next.js 15+)
typescript// app/forbidden.tsx import Link from 'next/link'; export default function Forbidden() { return ( <div className="flex flex-col items-center justify-center min-h-screen"> <h1 className="text-4xl font-bold text-red-600">Access Denied</h1> <p className="mt-4 text-gray-600"> You don't have permission to view this page. </p> <div className="mt-8 space-x-4"> <Link href="/dashboard" className="px-4 py-2 bg-blue-500 text-white rounded" > Go to Dashboard </Link> <Link href="/support" className="px-4 py-2 border border-gray-300 rounded" > Request Access </Link> </div> </div> ); }
Triggering the Forbidden Page
typescriptimport { forbidden } from 'next/navigation'; export default async function AdminPage() { const session = await getSession(); if (session?.user.role !== 'admin') { forbidden(); // Shows app/forbidden.tsx } return <AdminDashboard />; }
Unauthorized Page (Next.js 15+)
typescript// app/unauthorized.tsx import Link from 'next/link'; export default function Unauthorized() { return ( <div className="flex flex-col items-center justify-center min-h-screen"> <h1 className="text-4xl font-bold">Please Sign In</h1> <p className="mt-4 text-gray-600"> You need to be logged in to access this page. </p> <Link href="/login" className="mt-8 px-6 py-3 bg-blue-500 text-white rounded-lg" > Sign In </Link> </div> ); }
Route-Specific Error Pages
app/
admin/
forbidden.tsx # Admin-specific forbidden page
page.tsx
dashboard/
forbidden.tsx # Dashboard-specific forbidden page
page.tsx
forbidden.tsx # Global fallback
Custom Unauthorized Route
For more control, create a dedicated route:
typescript// app/unauthorized/page.tsx import { headers } from 'next/headers'; import Link from 'next/link'; export default function UnauthorizedPage() { const headersList = await headers(); const referer = headersList.get('referer'); const attemptedPath = referer ? new URL(referer).pathname : null; return ( <div className="max-w-md mx-auto mt-20 p-6"> <h1 className="text-2xl font-bold text-red-600">Access Denied</h1> {attemptedPath && ( <p className="mt-4 text-gray-600"> You tried to access <code className="bg-gray-100 px-2 py-1 rounded"> {attemptedPath} </code> </p> )} <div className="mt-6 space-y-4"> <p className="text-gray-600">This could mean:</p> <ul className="list-disc list-inside space-y-2 text-gray-600"> <li>Your account doesn't have the required permissions</li> <li>You need to log in with a different account</li> <li>This resource has restricted access</li> </ul> </div> <div className="mt-8 flex space-x-4"> <Link href="/dashboard" className="btn-primary"> Go to Dashboard </Link> <Link href="/login" className="btn-secondary"> Switch Account </Link> </div> </div> ); }
Common Pitfalls
-
Leaking information: Don't say "This admin page exists but you can't see it." Consider showing 404 for highly sensitive resources.
-
No path forward: Always give users something to do—go back, contact support, request access.
-
Generic styling: Error pages should match your app's design, not look like default browser errors.
Best Practices
- Be helpful, not punishing: Explain what happened and what to do next
- Don't leak resource existence: For sensitive resources, 404 might be better than 403
- Use route-specific pages: Admin forbidden page can differ from dashboard forbidden page
- Track forbidden attempts: Log when users hit permission errors for security monitoring
Summary
Custom forbidden and unauthorized pages improve UX when users lack permission. Use Next.js 15's forbidden() and unauthorized() functions to trigger them, create route-specific variants for different sections, and always provide helpful next steps. Good error pages reduce support burden and guide users appropriately.