Mastering Error and NotFound Boundaries in Next.js 15

Discover how to handle errors and missing routes gracefully in Next.js 15 using the new Error and NotFound boundaries. This post covers practical examples, common pitfalls and tips to keep your app resilient and user-friendly.

Picture of Ferhat Kefsiz

Ferhat Kefsiz

on

July 14, 2025

101 views
Mastering Error and NotFound Boundaries in Next.js 15

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:

src/app/posts/[id]/page.tsx
1
import { notFound } from "next/navigation"
2
3
export default async function Page({ params }) {
4
const post = await getPostById(params.id)
5
if (!post) notFound() // Trigger 404 UI if post is missing
6
return <h1>{post.title}</h1>
7
}

Then, alongside this page, you create app/blog/[id]/not-found.tsx:

src/app/posts/[id]/not-found.tsx
1
export default function NotFound() {
2
return <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:

  1. 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:
tsx
1
"use client" // must be a client component
2
export default function Error({
3
error,
4
reset,
5
}: {
6
error: Error
7
reset: () => void
8
}) {
9
// (Optional) Log error for debugging
10
console.error(error)
11
return (
12
<div style={{ padding: 20, textAlign: "center" }}>
13
<h2>Something went wrong!</h2>
14
<button onClick={() => reset()} style={{ marginTop: 10 }}>
15
Try Again
16
</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)

  1. 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:
app/blog/[slug]/page.tsx
1
import { notFound } from "next/navigation"
2
3
export default async function BlogPage({
4
params,
5
}: {
6
params: { slug: string }
7
}) {
8
const post = await getPostBySlug(params.slug)
9
if (!post) {
10
// Trigger 404 page if data is missing
11
notFound()
12
}
13
return (
14
<article>
15
<h1>{post.title}</h1>...
16
</article>
17
)
18
}

Next.js will then look for app/blog/[slug]/not-found.js.

  1. 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:
app/blog/[slug]/not-found.tsx
1
export default function NotFound() {
2
return (
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.

  1. (Optional) Global fallbacks. If you want a site-wide catch-all, you can put app/not-found.tsx (root 404) and app/error.tsx or app/global-error.tsx (root error). A global-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

FeaturePages DirectoryApp Directory
Handling 404snext/errornotFound() function
Custom 404 Pagepages/404.jsapp/not-found.tsx
Custom Error Handlingnext/error or throwing JavaScript errorserror.tsx with automatic error boundaries
Client vs Server ComponentsMostly client-renderedMix of server and client components