| | |

React Router 04 ⚛️ Fallback Routes — Handling 404 not found pages

⏱️ Estimated read: 11 min | |

The previous chapter covered how the router matches a URL against the route objects. This chapter covers the case where no route matches. The fallback route is the mechanism for handling unknown URLs. Without it, an unmatched URL renders nothing—an empty page with no navigation, no error message, and no way to recover except editing the URL manually. The LFCS exam does not test React Router, but front-end engineering interviews and certification paths do. The critical distinction is between the wildcard route (path: "*") and the error boundary (ErrorBoundary). The wildcard route handles the case where the URL does not match any route. The error boundary handles the case where the matched route throws an error during rendering, loading, or action execution. The two are not interchangeable.

Key point: The fallback route is a route object with path: "*". It matches any URL that the other routes do not match. It must be placed at the bottom of the children array (or at the root level) so that the specific routes are matched first. The fallback renders a “not found” component with a link back to the home page. The ErrorBoundary is the separate mechanism for errors thrown by the matched route, not for unmatched URLs.


Why fallback routes require a specific mental model

The empty page problem. A user navigates to /products/nonexistent. The route array has /products/:id, which matches the URL. The component renders, calls a loader or fetches data, and receives a 404 from the server. The component must handle the error and render a “not found” message. This is different from a URL that matches no route at all, such as /nonexistent/path. The route array has no :id for that path, and the router has nothing to render. The wildcard route is the mechanism for the second case .

The path: "*" problem. The wildcard route is a route object with path: "*". It matches any URL that the other routes do not match. The router’s ranking system gives it the lowest priority, so it does not shadow the specific routes. The params["*"] contains the unmatched path, which is useful for logging or for displaying the path in the 404 message .

The placement problem. The wildcard route must be placed after the specific routes in the children array. The router’s ranking is based on specificity, not on the array order, but the placement matters for readability and for the mental model. A wildcard route at the top of the array is confusing, even though the ranking ensures that the specific routes still match. The convention is to place the wildcard at the bottom, with a comment that identifies it as the fallback.

The error boundary problem. The ErrorBoundary catches errors thrown by the matched route’s loader, action, or component. It does not catch unmatched URLs. A loader that throws a 404 response (from a Response object) is caught by the ErrorBoundary, and the boundary component uses useRouteError to inspect the status. The wildcard route is for URLs that match no route. The error boundary is for routes that match but fail. The two mechanisms are complementary .

The trade-off. The wildcard route is the simplest fallback. The error boundary is the more sophisticated mechanism that handles the failure of a matched route. A complete application has both: the wildcard for unknown URLs, and the error boundary for failed loaders and components. The exam tests the distinction and the placement of the wildcard route.


a. The wildcard route and the NotFound component

The wildcard route is a route object with path: "*". It renders the NotFound component when no other route matches.

import { createBrowserRouter } from "react-router";
import { Link } from "react-router";

function NotFound() {
  return (
    <div>
      <h1>404 — Page Not Found</h1>
      <p>The page you are looking for does not exist.</p>
      <Link to="/">Go back to home</Link>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    children: [
      { index: true, Component: Home },
      { path: "about", Component: About },
      { path: "users/:id", Component: UserProfile },
      { path: "*", Component: NotFound },
    ],
  },
]);

The NotFound component is a regular React component. It renders a heading, a message, and a <Link> back to the home page. The <Link> is the navigation mechanism that uses the router’s history API, so it does not trigger a full page reload .

The params["*"] contains the unmatched path. The NotFound component can display it.

import { useParams } from "react-router";

function NotFound() {
  const { "*": path } = useParams();
  return (
    <div>
      <h1>404 — Not Found</h1>
      <p>No match for: /{path}</p>
      <Link to="/">Home</Link>
    </div>
  );
}

The path is useful for the user to see what they typed and for the developer to log the error.


b. Placement and the catch-all at the root level

The wildcard route can be placed at the root level or as a child of the root route. The placement determines the layout that wraps the 404 page.

If the wildcard is a child of the root route, the Root component renders the navigation, and the NotFound component renders inside the <Outlet />. The user sees the 404 message with the site’s navigation. This is the preferred placement for most applications.

{
  path: "/",
  Component: Root,
  children: [
    { index: true, Component: Home },
    { path: "about", Component: About },
    { path: "*", Component: NotFound },
  ],
}

If the wildcard is at the root level, the NotFound component renders without the Root layout. This is the correct placement for a standalone 404 page that does not need the navigation, such as a maintenance page.

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    children: [
      { index: true, Component: Home },
    ],
  },
  {
    path: "*",
    Component: Standalone404,
  },
]);

The root-level wildcard is the fallback for URLs that do not match the root route at all. The child wildcard is the fallback for URLs that match the root route but no child. The difference is whether the root layout wraps the 404 page.


c. The error boundary versus the wildcard

The ErrorBoundary is the mechanism for errors thrown by the matched route. A loader that throws a 404 response is caught by the boundary, and the boundary component renders the error message.

import { useRouteError } from "react-router";

function RootErrorBoundary() {
  const error = useRouteError();
  if (error.status === 404) {
    return (
      <div>
        <h1>Resource not found</h1>
        <Link to="/">Home</Link>
      </div>
    );
  }
  return (
    <div>
      <h1>Error</h1>
      <p>{error.message}</p>
    </div>
  );
}

The error.status is available when the loader throws a Response object with a status code. The error.message is available for other errors. The boundary component decides which message to show based on the status.

The wildcard route and the error boundary are complementary. The wildcard handles URLs that match no route. The error boundary handles routes that match but fail. A complete application has both.

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    ErrorBoundary: RootErrorBoundary,
    children: [
      { index: true, Component: Home },
      {
        path: "users/:id",
        Component: UserProfile,
        loader: async ({ params }) => {
          const res = await fetch(`/api/users/${params.id}`);
          if (!res.ok) throw new Response("Not Found", { status: 404 });
          return res.json();
        },
        ErrorBoundary: UserErrorBoundary,
      },
      { path: "*", Component: NotFound },
    ],
  },
]);

The UserProfile route has its own ErrorBoundary that catches the 404 from the loader. The wildcard route catches the URLs that do not match users/:id or any other route. The two mechanisms handle different cases .


Complete Example Session

# ============================================
# PART 1: CREATE THE NOT FOUND COMPONENT
# ============================================

import { Link, useParams } from "react-router";

function NotFound() {
  const { "*": path } = useParams();
  return (
    <div>
      <h1>404 — Not Found</h1>
      <p>No match for: /{path}</p>
      <Link to="/">Home</Link>
    </div>
  );
}
# ============================================
# PART 2: ADD THE WILDCARD ROUTE
# ============================================

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    children: [
      { index: true, Component: Home },
      { path: "about", Component: About },
      { path: "*", Component: NotFound },
    ],
  },
]);
# ============================================
# PART 3: VERIFY THE 404
# ============================================

# Navigate to /nonexistent → NotFound renders.
# ============================================
# PART 4: ADD THE ERROR BOUNDARY
# ============================================

function RootErrorBoundary() {
  const error = useRouteError();
  if (error.status === 404) {
    return <NotFound />;
  }
  return <h1>Error: {error.message}</h1>;
}

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    ErrorBoundary: RootErrorBoundary,
    children: [...],
  },
]);
# ============================================
# PART 5: THROW FROM A LOADER
# ============================================

loader: async ({ params }) => {
  const res = await fetch(`/api/users/${params.id}`);
  if (!res.ok) throw new Response("Not Found", { status: 404 });
  return res.json();
}
# ============================================
# PART 6: VERIFY THE LOADER ERROR
# ============================================

# Navigate to /users/999 → loader throws 404 → RootErrorBoundary renders.
# ============================================
# PART 7: LOG THE UNMATCHED PATH
# ============================================

function NotFound() {
  const { "*": path } = useParams();
  useEffect(() => {
    console.error("Unmatched path:", path);
  }, [path]);
  return <h1>404</h1>;
}
# ============================================
# PART 8: ROOT-LEVEL WILDCARD
# ============================================

const router = createBrowserRouter([
  { path: "/", Component: Root, children: [...] },
  { path: "*", Component: Standalone404 },
]);
# ============================================
# PART 9: TEST ALL CASES
# ============================================

# /unknown/path → root wildcard
# /users/999 → loader throws → error boundary
# /about → About component

The nine parts trace the fallback mechanism: NotFound component, wildcard route, verification, error boundary, loader throw, verification, logging, root-level wildcard, and testing.


Quick Reference

Wildcard Route

PropertyValue
path"*"
MatchAny URL not matched by other routes
params["*"]The unmatched path

Error Boundary

PropertyPurpose
ErrorBoundaryCatches errors from matched routes
useRouteErrorProvides the error object
error.statusHTTP status from Response throw

Placement

PlacementEffect
Child of root404 renders inside root layout
Root-level404 renders standalone

Error Types

ErrorCaught By
Unmatched URLWildcard route
Loader throws 404ErrorBoundary
Component throwsErrorBoundary
Action throwsErrorBoundary

Best Practices

✅ Do This:

{ path: "*", Component: NotFound }
<!-- Add a wildcard route for unmatched URLs -->      # ✅
ErrorBoundary: RootErrorBoundary
<!-- Add an error boundary for matched routes -->     # ✅
const { "*": path } = useParams();
<!-- Show the unmatched path in the 404 -->           # ✅

❌ Don’t Do This:

# Assume the wildcard catches loader errors
<!-- The wildcard handles unmatched URLs only -->    # ❌
# Place the wildcard at the top of the array
<!-- Confusing, even if ranking is correct -->       # ❌
# Forget the Link back to home
<!-- The user has no way to recover -->              # ❌

Common Pitfalls

PitfallWhy It HappensFix
404 page blankNo wildcard routeAdd path: "*"
404 page without layoutWildcard at root levelMove to child of root
Loader error not caughtNo ErrorBoundaryAdd the boundary
params["*"] emptyWildcard not matchedCheck the route path
Navigation from 404 failsUsed <a> instead of <Link>Use Link

Real-World Examples

1. NotFound Component

function NotFound() { return <h1>404</h1>; }

2. Wildcard Route

{ path: "*", Component: NotFound }

3. Access Path

const { "*": path } = useParams();

4. Error Boundary

function ErrorBoundary() { const error = useRouteError(); return <h1>{error.message}</h1>; }

5. Loader Throw

if (!res.ok) throw new Response("Not Found", { status: 404 });

6. Root-Level Wildcard

{ path: "*", Component: Standalone404 }

7. Link Home

<Link to="/">Home</Link>

8. Error Status

if (error.status === 404) return <NotFound />;

9. Log Path

console.error("Unmatched:", path);

10. Test Routes

/unknown → NotFound
/users/999 → loader error → ErrorBoundary
/about → About

Visual

Wildcard vs Error Boundary

┌──────────────────────────────────────────────┐
│  URL: /unknown/path                          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  No route matches                            │
│  → Wildcard route → NotFound                 │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  URL: /users/999                             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Route matches /users/:id                    │
│  → Loader throws 404                          │
│  → ErrorBoundary → NotFound                  │
└──────────────────────────────────────────────┘

Placement

┌──────────────────────────────────────────────┐
│  Root route with children                    │
│  ├── Home (index)                            │
│  ├── About                                   │
│  ├── Users                                   │
│  └── * → NotFound (inside root layout)       │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Root-level wildcard (outside root layout)   │
│  └── * → Standalone404                       │
└──────────────────────────────────────────────┘

Summary

ItemValue
Wildcard path"*"
MatchUnmatched URLs
Unmatched path accessuseParams()["*"]
Error boundaryErrorBoundary: Component
Error accessuseRouteError()
Error statuserror.status
Child wildcardInside root layout
Root wildcardStandalone

Key takeaways:

  • The wildcard route handles unmatched URLs. The path: "*" matches any URL that the other routes do not match. The params["*"] contains the unmatched path, which is useful for logging and for displaying the path in the 404 message .
  • The error boundary handles errors from matched routes. The ErrorBoundary catches errors thrown by loaders, actions, and components. The useRouteError hook provides the error object. The wildcard and the boundary are complementary, not interchangeable .
  • Placement determines the layout. A wildcard inside the root route’s children renders inside the root layout. A root-level wildcard renders without the layout. The choice depends on whether the 404 page needs the site navigation .
  • The loader can throw a Response to trigger the error boundary. The throw new Response("Not Found", { status: 404 }) is the mechanism for a resource that does not exist. The error.status in the boundary identifies the 404 case .
  • The <Link> is the recovery mechanism. The 404 page must have a link back to the home page. The <Link> uses the router’s history, not a full page reload.

Remember: The wildcard is the URL fallback. The error boundary is the route failure fallback. The "*" is the unmatched URL. The ErrorBoundary is the thrown error. The Link is the recovery. The exam tests the distinction and the placement.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!