Back to Blog
Lesson 14 of the Next.js: Build Full-Stack Apps with the App Router course
Next.jsAugust 1, 20264 min read

Handling 404 Not Found Pages in Next.js App Router

Learn to implement custom 404 pages in Next.js using not-found.js. Improve your site's UX by gracefully handling missing routes and missing data.

Next.js404UXWeb DevelopmentRouting
Abstract shot of a smartphone screen with blurred images and text, creating an artistic effect.

Previously in this course, we explored handling errors with error.js, which manages runtime exceptions in your application. While error.js catches unexpected crashes, sometimes the "error" isn't a bug—it’s simply that the requested resource doesn't exist.

In this lesson, we’ll implement custom 404 Not Found pages. Providing a helpful, branded, and intuitive experience for missing pages is a cornerstone of professional UX, ensuring users don't feel lost when they navigate to a broken link.

Understanding the 404 Lifecycle

In the Next.js App Router, there are two primary ways a user might end up on a "Not Found" page:

  1. Automatic: The user visits a URL that does not match any route defined in your app directory.
  2. Programmatic: The user visits a valid route (like /blog/my-post), but the underlying data (the post) is missing from your database.

Next.js provides a specialized file, not-found.js, to handle both scenarios consistently.

Implementing not-found.js

The not-found.js file works similarly to layout.js or page.js. When placed in your directory structure, it acts as a global (or scoped) fallback.

1. Creating the Global Not Found Page

Create a file named app/not-found.js in the root of your project. This will catch any route that doesn't exist in your application.

JSX
import Link from CE9178">'next/link';

export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-[50vh] text-center">
      <h2 className="text-4xl font-bold mb-4">404 - Page Not Found</h2>
      <p className="mb-8 text-gray-600">
        Oops! We couldn't find the page you were looking for.
      </p>
      <Link 
        href="/" 
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        Return Home
      </Link>
    </div>
  );
}

2. Triggering 404 Programmatically

Sometimes, a route exists, but the data does not. For example, if you have a dynamic route /blog/[slug], the route segment exists, but a specific slug might not match any record in your database.

To trigger the "Not Found" UI from a Server Component, you use the notFound function from next/navigation.

JSX
// app/blog/[slug]/page.js
import { notFound } from CE9178">'next/navigation';

export default async function BlogPost({ params }) {
  const post = await getPostFromDatabase(params.slug);

  if (!post) {
    // This triggers the nearest not-found.js
    notFound();
  }

  return <h1>{post.title}</h1>;
}

When to use 404 vs. Error Boundaries

It is important to distinguish between these two concepts to keep your code clean and your users happy.

ScenarioToolWhy?
Route/Data Missingnot-found.jsExpected behavior for invalid user input.
Unexpected Crasherror.jsUnexpected runtime exceptions (e.g., DB connection loss).

Hands-on Exercise

  1. Create the file: Add app/not-found.js to your project root.
  2. Style it: Use the Tailwind CSS classes we learned in our previous lesson to match your blog's aesthetic.
  3. Test the trigger:
    • Visit a non-existent URL (e.g., localhost:3000/broken-link) to see your global 404 page.
    • Go to your dynamic blog post page (/blog/[slug]) and force a notFound() call when a dummy slug (like /blog/does-not-exist) is provided to see it in action.

Common Pitfalls

  • Forgetting not-found.js is not a catch-all for errors: Do not use notFound() to handle API failures or database timeouts. notFound() is specifically for "The resource exists in the real world, but not in our database."
  • Missing Imports: Ensure you import notFound from next/navigation, not next/router.
  • Layout Conflicts: Remember that not-found.js will be rendered inside your root layout.js. If you have a complex sidebar or header, it will remain visible—which is usually the desired behavior for a consistent UX.

FAQ

Can I have multiple not-found pages? Yes. You can place not-found.js inside specific route segments (e.g., app/blog/not-found.js) to provide custom messaging specific to that section of your site.

Does notFound() stop execution? Yes. When you call notFound(), it throws an error that aborts rendering for that specific route segment, ensuring no further code in your component executes.

Is this related to technical SEO? Absolutely. Properly returning a 404 status code (which Next.js handles automatically when you use not-found.js) is essential for fixing basic technical issues and maintaining a healthy site index.

Recap

In this lesson, we added a layer of professionalism to our blog by handling missing content. We implemented not-found.js for automatic routing errors and leveraged notFound() for data-specific missing resources. These practices ensure your users always have a clear path forward, even when they reach a dead end.

Up next: We will begin building reusable UI components to keep our blog codebase DRY and maintainable.

Similar Posts