Customizing Error Pages: Branding and Handling Errors in Next.js
Learn to master error handling, UI design, and branding for your Next.js application. Create professional 404 and 500 pages that keep users engaged.
Previously in this course, we explored Incremental Static Regeneration (ISR) to keep our content fresh and performant. In this lesson, we add a crucial layer of polish: custom error handling. When your application encounters a hiccup—whether it’s a missing page or a server-side crash—the default white browser screen is a poor user experience. We’ll design branded error states that maintain trust and guide your users back to safety.
Understanding Error Boundaries and Global States
In the App Router, Next.js provides a hierarchical approach to error management. Instead of letting an application crash entirely, we use "Error Boundaries." An error.js file creates a boundary that catches errors in its child route segments.
Think of it as a circuit breaker. If a component deep in your tree fails, the error bubbles up to the nearest error.js file, which replaces that section of the UI with a fallback component, while the rest of the page remains functional.
To build a professional, cohesive experience, you need to address two primary types of failures:
- 404 (Not Found): The user requested a resource that doesn't exist.
- 500 (Internal Server Error): Something went wrong on your server or in your code.
Designing Branded Error UI
Branding is about consistency. If your site uses a specific color palette, font, and layout, your error pages should reflect that. We’ll use Tailwind CSS to ensure our error pages feel like an intentional part of the application.
Create a global not-found.js file in your app/ directory. This acts as the catch-all for any request that doesn't match an active route.
TSX// app/not-found.tsx import Link from CE9178">'next/link'; export default function NotFound() { return ( <div className="flex flex-col items-center justify-center min-h-[60vh] text-center"> <h2 className="text-4xl font-bold text-slate-900">Page Not Found</h2> <p className="mt-4 text-slate-600">Sorry, we couldnCE9178">'t find the page you're looking for.</p> <Link href="/" className="mt-6 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" > Return Home </Link> </div> ); }
Implementing Global Error Boundaries
While not-found.js handles missing routes, error.js handles runtime exceptions. By placing an error.tsx in your app/ directory, you create a root error boundary that catches any unhandled errors in your application.
TSX// app/error.tsx CE9178">'use client'; import { useEffect } from CE9178">'react'; export default function Error({ error, reset }: { error: Error; reset: () => void }) { useEffect(() => { // Log the error to your monitoring service console.error(error); }, [error]); return ( <div className="flex flex-col items-center justify-center min-h-[60vh]"> <h2 className="text-2xl font-bold">Something went wrong!</h2> <button onClick={() => reset()} className="mt-4 px-4 py-2 bg-red-600 text-white rounded" > Try again </button> </div> ); }
Because error.js must be a Client Component, we add the 'use client' directive at the top. The reset function provided by Next.js attempts to re-render the component tree, allowing the user to recover from transient issues without a hard page refresh.
Testing Your Error States
Testing error UI is often overlooked, but it is critical. You can simulate a 500 error in a development environment by intentionally throwing an error in a page:
TSX// app/blog/page.tsx export default function BlogPage() { throw new Error("Failed to load blog posts"); // ... }
When you navigate to /blog, your custom error.tsx will trigger. This is your chance to verify that your layout, navigation, and branding appear exactly as you intended.
Common Pitfalls
- Forgetting 'use client':
error.jsmust be a Client Component. If you forget this, Next.js will throw a build error. - Over-relying on
error.js: Don't use it to replace proper validation. As discussed in Error Handling Best Practices: Clean API Design and Debugging, you should handle expected data errors (like failed form submissions) with explicit logic before they reach the boundary. - Infinite loops: If your
error.tsxitself contains code that throws an error, you will crash the UI. Keep your error components simple and dependency-free.
FAQ
Can I have multiple error boundaries?
Yes. You can nest error.js files in different subdirectories. The error will bubble up to the nearest one.
Does not-found.js catch errors?
No, not-found.js is specifically for 404s. It is triggered when you call the notFound() function or when a route segment is not found.
Should I log errors in production?
Absolutely. Always integrate a monitoring service like Sentry or LogRocket in your useEffect inside error.tsx to gain visibility into what your users are experiencing. Learn more about professional logging patterns in Exception Handling Best Practices: Clean Code & Debugging.
Recap
We've replaced unhelpful browser defaults with a branded, professional error experience. By using not-found.js for missing routes and error.js for runtime failures, we ensure our blog project remains resilient and user-friendly.
Up next: We'll expand our blog's reach by implementing an automated RSS feed.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js — the kind that loads instantly and ranks. Design-to-code, done right.
