Introduction
Error handling used to be something I just “added later.” You know, once everything was working… kind of. But with Next.js 15, things have changed—Error Boundaries and NotFound Boundaries are no longer afterthoughts. They’re part of the design.
Let me walk you through how I started using these boundaries properly in my Next.js 15 projects. No overcomplicated setups, just practical examples, real-world structure, and the kind of stuff I wish I had seen earlier.
Whether you're new to the app router or just tired of vague “500 Something went wrong” pages, this guide will help you build smoother experiences—for both users and developers.
Why Error Handling Matters?
- Better User Experience: Users don’t want to see blank screens or scary error messages. Proper error handling lets you show a helpful message, like “Oops, something went wrong!” instead of a broken page.
- Improved Debugging: When errors are caught and logged, it makes debugging much easier for developers. You can quickly identify the issue and fix it.
- SEO & Performance: If your app crashes or shows broken pages, search engines like Google might rank your site lower. Handling errors properly helps keep your app smooth and reliable.
Understanding the Basics: Error vs NotFound Boundaries
First, Error boundaries. If any React component in a route throws an uncaught exception, Next.js looks for the nearest error.js/tsx
in that folder (or parent folders) to render instead of crashing the whole app. In other words, your error.js
component wraps the segment and shows UI when something unexpected happens. It must be a client component (include "use client"
at the top) and accepts two props: an error
object and a reset()
function. You typically log the error and render a “Something went wrong” message with a retry button. The reset callback re-renders the segment when clicked, letting you attempt recovery.
Error boundaries are for unexpected bugs or runtime failures (uncaught exceptions). They do not catch errors in event handlers or asynchronous callbacks – those you’d handle manually with try/catch or React state. Also note: errors in one boundary won’t crash other parts of your app. They “bubble up” to the nearest error boundary, so you can place multiple boundaries for fine-grained control.
Now, NotFound boundaries. These are for expected “missing” errors – like trying to load a blog post that doesn’t exist. In your server-side code (e.g. a page or layout), you can import notFound()
from next/navigation
. Calling notFound() immediately stops rendering and triggers the nearest not-found.js/tsx
file in that segment. For example, you might do something like:
1import { notFound } from "next/navigation"23export default async function Page({ params }) {4const post = await getPostById(params.id)5if (!post) notFound() // Trigger 404 UI if post is missing6return <h1>{post.title}</h1>7}
Then, alongside this page, you create app/blog/[id]/not-found.tsx
:
1export default function NotFound() {2return <h1>404 - Post Not Found</h1>3}
When notFound()
is called, Next.js will render that NotFound component. (If you omit a not-found
file, a default Next.js 404 page shows.)
In practice, a NotFound boundary renders a friendly 404 page (like the “Product not found” example above) instead of a blank page. By default, put one app/not-found.tsx
at the root to catch all unmatched URLs (making it your global 404). You can also place not-found.tsx
files in subfolders for section-specific 404s – for example, app/products/[slug]/not-found.tsx
to only catch missing products. These not-found.tsx components don’t receive any props, but you can make them async server components if you want to fetch related data (e.g. “Popular products” to suggest)
In summary: error boundaries (error.js/tsx
) are client components that catch runtime errors and show a fallback UI. NotFound boundaries (not-found.js/tsx) show a 404 UI when you call notFound() for a missing resource. Both are scoped to the app folder they live in, so your site can have multiple levels of error/404 pages.
Setting Up Your error.js
and not-found.js
Files
Here’s how you actually create these boundaries in Next.js 15:
- Create an
error.js/tsx
in the route folder. For example,app/dashboard/error.tsx
. Inside it, export a React component (this must be a Client Component, so include"use client"
at the top). It receives{ error, reset }
as props. A simple example:
1"use client" // must be a client component2export default function Error({3error,4reset,5}: {6error: Error7reset: () => void8}) {9// (Optional) Log error for debugging10console.error(error)11return (12<div style={{ padding: 20, textAlign: "center" }}>13<h2>Something went wrong!</h2>14<button onClick={() => reset()} style={{ marginTop: 10 }}>15Try Again16</button>17</div>18)19}
In this setup, if any child component in /dashboard throws, this UI renders instead. The reset() function tells Next.js to retry rendering the segment. (This pattern comes straight from the Next.js docs)
- Call
notFound()
in your data or page logic. Wherever you fetch or generate content (typically in a page or layout), check if the data exists. If it doesn’t, call notFound() from next/navigation. For example:
1import { notFound } from "next/navigation"23export default async function BlogPage({4params,5}: {6params: { slug: string }7}) {8const post = await getPostBySlug(params.slug)9if (!post) {10// Trigger 404 page if data is missing11notFound()12}13return (14<article>15<h1>{post.title}</h1>...16</article>17)18}
Next.js will then look for app/blog/[slug]/not-found.js.
- Add a not-found.js/tsx file. In the same folder as the page, create not-found.tsx (or .js). Export a simple component that renders your 404 content. Example:
1export default function NotFound() {2return (3<div style={{ padding: 20, textAlign: "center" }}>4<h1>404 – Oops, not here!</h1>5<p>Sorry, we couldn’t find that page.</p>6</div>7)8}
When notFound()
is called, this component shows up. The Next.js docs demonstrate this pattern for blog posts. You can style it however you like.
- (Optional) Global fallbacks. If you want a site-wide catch-all, you can put
app/not-found.tsx
(root 404) andapp/error.tsx
orapp/global-error.tsx
(root error). Aglobal-error.tsx
replaces your entire app layout (it must include its own<html><body>
tags). Most apps only need one root 404 and maybe a root error, plus segment-specific ones as needed.
In short, treat error.js
and not-found.js
as you would custom 404 and error pages, but with segment-level granularity. Place them logically: top-level ones for catch-all behavior, and nested ones when a certain section needs its own error handling. And always cite the Next.js docs if you need reminders – they have solid examples of these patterns.
Differences between handling errors or 404 in the pages vs app directory
Feature | Pages Directory | App Directory |
---|---|---|
Handling 404s | next/error | notFound() function |
Custom 404 Page | pages/404.js | app/not-found.tsx |
Custom Error Handling | next/error or throwing JavaScript errors | error.tsx with automatic error boundaries |
Client vs Server Components | Mostly client-rendered | Mix of server and client components |