1.7 Error Handling in React Router
When building an enterprise-grade React application, setting up functional routes is only half the battle. A truly robust system architecture must handle real-world failures seamlessly.
This guide breaks down how to architect comprehensive error boundaries, access controls, and network resilience directly into your React Router implementation.
1. Page Not Found (404 Error) Architecture
Leaving users with an empty white screen when they type an incorrect URL ruins the user experience. Instead of haphazardly declaring fallback routes inside individual files, unhandled URLs should point globally to a dedicated NoPage component.
Implementing Global Wildcards via Feature Grouping
To maintain clean routing files, create an other.routes.jsx module to capture non-specific global states using the wildcard (*) path string.
// src/routes/other.routes.jsx
import NoPage from '../pages/other/NoPage';
export const otherRoutes = [
{
path: '*',
element: <NoPage />
}
];
Merge these configurations elegantly alongside your existing core feature arrays inside your central orchestration file:
// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import { authRoutes } from './routes/auth.routes';
import { userRoutes } from './routes/user.routes';
import { otherRoutes } from './routes/other.routes';
// Ensure the wildcard catch-all array is spread last
const allAppRoutes = [...authRoutes, ...userRoutes, ...otherRoutes];
function App() {
return (
<Routes>
{allAppRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
element={route.element}
/>
))}
</Routes>
);
}
2. Unauthorized Access Management (Protected & Role-Based Routes)
Checking whether a user is logged in or verifying their access level inside every single page file creates massive code repetition. The cleaner architectural solution is to build a centralized wrapper component that manages route access control globally.
// src/components/auth/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
export default function ProtectedRoute({ children, allowedRoles }) {
// Mock data mimicking an authentic identity payload from global state/storage
const user = {
isLoggedIn: true,
role: 'manager' // Alternative roles: 'admin', 'user'
};
// Condition 1: Check authentication status
if (!user?.isLoggedIn) {
return <Navigate to="/login" replace />;
}
// Condition 2: Validate authorization privileges
if (allowedRoles && !allowedRoles.includes(user?.role)) {
return <div className="error-container">Permission Denied: Access Restricted!</div>;
}
// If all guard rules pass, render the requested target page
return children;
}
Applying Guards to Route Definitions
Wrap feature elements inside your configurations with the guard component to protect specific URLs:
// src/routes/user.routes.jsx
import ProtectedRoute from '../components/auth/ProtectedRoute';
import UserList from '../pages/user/UserList';
export const userRoutes = [
{
path: '/user-list',
element: (
<ProtectedRoute allowedRoles={['admin']}>
<UserList />
</ProtectedRoute>
)
}
];
3. Handling Network Latency & Failures in Lazy Loading
Lazy loading code-splits your application into smaller visual chunks. However, if a user is browsing on a slow 3G cellular network, loading a new page chunk dynamically can cause noticeable delays or app freezes if the file fails to fetch.
To prevent this, combine Lazy Loading with React’s structural <Suspense> boundary to guarantee immediate feedback.
// src/App.jsx
import { Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import SuspenseLoader from './components/feedback/SuspenseLoader';
import { allAppRoutes } from './routes';
function App() {
return (
// Fallback renders instantly while dynamic bundles stream over the network
<Suspense fallback={<SuspenseLoader />}>
<Routes>
{allAppRoutes.map((route, index) => (
<Route key={index} path={route.path} element={route.element} />
))}
</Routes>
</Suspense>
);
}
4. Preventing Runtime Crashes (Optional Chaining)
The most common cause of application crashes occurs during data loading stages—such as trying to read properties from an object that turns out to be undefined or null (e.g., user.role).
Always use optional chaining (?.) when dealing with state properties that depend on an external server response or authentication token.
// ❌ Dangerous: App crashes instantly if the user object is null/missing
if (user.role === 'admin') { ... }
// Safe: Evaluates safely to undefined without halting execution
if (user?.role === 'admin') { ... }
Interview Q&A
Q1: What happens if a user visits a URL that doesn't exist in our React app? How do we handle it?
Answer: By default, React Router won't display anything or might show a blank space if a URL doesn't match our definitions. To fix this, we map a special route path using a wildcard asterisk (*). This path acts like a safety net that catches all unlisted URLs and redirects the user to a friendly custom "404 Page Not Found" screen.
Q2: How do you protect certain pages from being viewed by logged-out users?
Answer: Instead of checking for user sessions inside every individual component, we create a reusable wrapper component called a ProtectedRoute. We wrap our secret pages inside this component. When a user tries to access a restricted path, the wrapper checks if they are logged in. If they aren't, it immediately redirects them back to the login page.
Q3: Why is a <Suspense> wrapper mandatory when dealing with code-split (lazy-loaded) pages?
Answer: Because lazy-loaded pages aren't downloaded when the website first loads; they are fetched on demand when a user clicks a link. If the user's internet is slow, there will be a brief delay while the file downloads. <Suspense> allows us to specify a temporary loading layout (like a spinner) to show during this delay, keeping the application smooth and interactive.
Q4: How does Optional Chaining (?.) prevent runtime crashes during routing or state transitions?
Answer: Sometimes, user profile data or authorization details take a split second to load from the server, making the tracking object null or undefined momentarily. If our code tries to read data from an empty object, the app will crash instantly. Using the question-mark dot syntax tells JavaScript to safely skip reading the sub-property if the parent object isn't ready yet, preventing ugly crashes.