# Custom 404 Page Next.js 14: Common Pitfalls Preventing Proper Rendering

> Fix your custom 404 page in Nextjs 14. Learn common rendering errors like incorrect file naming, missing exports, and conflicting data fetches. Ensure your not found pages work.

- Repository: [Vercel/next.js](https://github.com/vercel/next.js)
- Tags: how-to-guide
- Published: 2026-02-20

---

**The most common reasons a custom 404 page fails to render in Next.js 14 are incorrect file naming conventions between the Pages and App Routers, missing default exports, and returning both `notFound` and `redirect` from data-fetching functions.**

Next.js 14 supports two distinct routing architectures: the legacy **Pages Router** (`pages/…`) and the modern **App Router** (`app/…`). Each system implements its own convention for handling 404 errors, and the framework selects which component to render through a series of internal validations defined in the [`vercel/next.js`](https://github.com/vercel/next.js/blob/main/vercel/next.js) source code. If any validation fails, Next.js falls back to the built-in `NotFound` component located at [`packages/next/src/client/components/builtin/not-found.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/client/components/builtin/not-found.tsx) instead of your custom implementation.

## File Naming and Location Conventions

The framework discovers your custom 404 page using specific filename patterns that differ between routing systems.

### Pages Router Convention

For the Pages Router, the file must be located at exactly `pages/404.{js,jsx,ts,tsx}`. The lookup logic in [`packages/next/src/server/lib/find-page-file.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/lib/find-page-file.ts) specifically searches for this pattern during the route discovery phase.

```tsx
// File: pages/404.tsx
export default function CustomNotFound() {
  return (
    <main style={{ padding: '2rem', textAlign: 'center' }}>
      <h1>Page Not Found</h1>
      <p>The requested resource does not exist.</p>
    </main>
  )
}

```

### App Router Convention

The App Router uses a different naming scheme. You must create `app/not-found.{js,jsx,ts,tsx}` for route-specific 404 boundaries, or `app/global-not-found.{js,jsx,ts,tsx}` for a global fallback. The regex `rootNotFoundFileRegex` in [`find-page-file.ts`](https://github.com/vercel/next.js/blob/main/find-page-file.ts) only matches these specific filenames.

```tsx
// File: app/not-found.tsx
export default function NotFound() {
  return (
    <section style={{ margin: '4rem' }}>
      <h2>Resource Unavailable</h2>
      <p>This route does not exist in the application.</p>
    </section>
  )
}

```

## Export Requirements and Component Validation

Even with correct file placement, your custom 404 page must satisfy strict export validation rules.

### Mandatory Default Export

The component tree builder in [`packages/next/src/server/app-render/create-component-tree.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/create-component-tree.tsx) validates that the module exports a default component. If the validation fails, the framework calls `errorMissingDefaultExport(..., 'not-found')` and discards your file, falling back to the built-in UI.

**Incorrect implementation:**

```tsx
// This will fail validation
export const NotFound = () => <div>404</div>

```

**Correct implementation:**

```tsx
// This satisfies the default export requirement
export default function NotFound() {
  return <div>404</div>
}

```

### Component Type Constraints

For the App Router, the `not-found` component can be a **React Server Component**, but it must return valid JSX. Exporting a plain object, class component without proper instantiation, or a client-only component without the `use client` directive causes a type mismatch that triggers the fallback mechanism.

## Data Fetching Conflicts

A critical pitfall occurs when data-fetching functions return conflicting directives that short-circuit the rendering pipeline.

### The notFound/redirect Exclusivity Rule

In [`packages/next/src/render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/render.tsx) (lines 917-925), the framework enforces that `getStaticProps` or `getServerSideProps` cannot return both `notFound: true` and a `redirect` object simultaneously. If both properties are present, Next.js throws an error and aborts the render, often resulting in the default error overlay instead of your custom 404 page.

**Problematic code:**

```tsx
// File: pages/posts/[slug].tsx
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug)
  
  if (!post) {
    // This will cause an error!
    return { 
      notFound: true,
      redirect: { destination: '/archive', permanent: false }
    }
  }
  
  return { props: { post } }
}

```

**Correct approach:**

```tsx
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug)
  
  if (!post) {
    // Return only notFound
    return { notFound: true }
  }
  
  return { props: { post } }
}

```

## Router Coexistence and Precedence

When migrating from Pages Router to App Router, conflicting 404 implementations can cause unexpected behavior.

### Precedence Rules

If both `pages/404` and `app/not-found` exist simultaneously, the App Router takes precedence for routes rendered under the `app` directory. However, legacy Pages Router routes still use `pages/404`. This split behavior can confuse debugging efforts when you expect a unified 404 experience across the application.

### Global vs. Route-Specific Fallbacks

The App Router supports [`app/global-not-found.tsx`](https://github.com/vercel/next.js/blob/main/app/global-not-found.tsx) as a catch-all fallback when no specific `not-found` boundary exists for a route segment. According to [`packages/next/src/server/app-render/app-render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/app-render.tsx), the framework builds a loader tree that prioritizes `global-not-found` over the built-in component, but only if the file exists and exports a valid default component.

## Static Export Considerations

When using `next export` to generate a static site, the build process maps the special route `/_not-found` to the static file [`404.html`](https://github.com/vercel/next.js/blob/main/404.html) (as implemented in [`packages/next/src/export/index.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/export/index.ts), lines 862-866).

### Verification Steps

After running the export command, verify that your custom markup appears in the generated file:

```bash
npm run build
npm run export

# Verify custom content exists

grep "Your custom 404 text" out/404.html

```

If the output shows the default Next.js 404 text instead of your custom content, the export step likely failed to detect your `pages/404` or `app/not-found` file during the build phase.

## Summary

- **File naming must match the router convention exactly**: Use [`pages/404.tsx`](https://github.com/vercel/next.js/blob/main/pages/404.tsx) for Pages Router and [`app/not-found.tsx`](https://github.com/vercel/next.js/blob/main/app/not-found.tsx) for App Router.
- **Always export a default component**: Named exports or missing exports trigger `errorMissingDefaultExport` and fall back to the built-in UI.
- **Never combine `notFound` and `redirect`**: Data-fetching functions must return exclusively one or the other to avoid runtime errors in [`render.tsx`](https://github.com/vercel/next.js/blob/main/render.tsx).
- **Check for router conflicts**: Coexisting `pages/404` and `app/not-found` files can cause inconsistent behavior depending on which router handles the request.
- **Verify static exports**: Ensure [`out/404.html`](https://github.com/vercel/next.js/blob/main/out/404.html) contains your custom markup after running `next export`, as the framework maps `/_not-found` to this file.

## Frequently Asked Questions

### Why does my custom 404 page show the default Next.js error UI instead of my component?

The framework validates your file against several rules in the source code. If your file is named incorrectly (e.g., [`404.tsx`](https://github.com/vercel/next.js/blob/main/404.tsx) in the `app` directory instead of [`not-found.tsx`](https://github.com/vercel/next.js/blob/main/not-found.tsx)), lacks a default export, or fails module type validation in [`create-component-tree.tsx`](https://github.com/vercel/next.js/blob/main/create-component-tree.tsx), Next.js falls back to the built-in `NotFound` component located at [`packages/next/src/client/components/builtin/not-found.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/client/components/builtin/not-found.tsx).

### Can I use `getStaticProps` or `getServerSideProps` with a custom 404 page in the App Router?

No. The App Router uses [`app/not-found.tsx`](https://github.com/vercel/next.js/blob/main/app/not-found.tsx) as a React Server Component or Client Component, and it does not support `getStaticProps` or `getServerSideProps`. Data fetching for error boundaries in the App Router should happen within the component itself using `fetch` or by passing props from parent layouts. The `getStaticProps`/`getServerSideProps` APIs are exclusive to the Pages Router.

### What happens if I have both [`pages/404.tsx`](https://github.com/vercel/next.js/blob/main/pages/404.tsx) and [`app/not-found.tsx`](https://github.com/vercel/next.js/blob/main/app/not-found.tsx) in the same project?

When both files exist, the App Router takes precedence for any routes served from the `app` directory, while the Pages Router continues to use [`pages/404.tsx`](https://github.com/vercel/next.js/blob/main/pages/404.tsx) for legacy pages. This can lead to inconsistent user experiences if the two components have different designs. To ensure uniformity, either migrate completely to the App Router and remove [`pages/404.tsx`](https://github.com/vercel/next.js/blob/main/pages/404.tsx), or keep both files synchronized with identical markup.

### Why does my static export show the default 404 page instead of my custom one?

During `next export`, the framework maps the internal route `/_not-found` to the output file [`404.html`](https://github.com/vercel/next.js/blob/main/404.html) (as defined in [`packages/next/src/export/index.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/export/index.ts) lines 862-866). If your custom [`pages/404.tsx`](https://github.com/vercel/next.js/blob/main/pages/404.tsx) or [`app/not-found.tsx`](https://github.com/vercel/next.js/blob/main/app/not-found.tsx) is not detected during the build phase—due to incorrect file naming, missing default export, or build errors—the export process falls back to the default built-in component. Always verify that [`out/404.html`](https://github.com/vercel/next.js/blob/main/out/404.html) contains your custom markup after the export completes.