Next.js course Β· Module 2: Routing and Layouts
Handling 404 and Redirects (not-found.js and redirects)
In this lesson6
In the vast quantum city of Metropolis Quantum, navigation becomes a key element of daily life. But what happens when a resident or tourist tries to reach a non-existent location? The city system must elegantly handle such a case - either informing that the place doesn't exist, or redirecting to an alternative location.
Similarly, in web applications built with Next.js 16, we need to elegantly handle situations when a user tries to access a non-existent page or resource. This chapter focuses on creating custom 404 pages and implementing redirects in Next.js 16.
Handling 404 (Not Found) Pages
In Next.js 16, we can handle 404 pages in two main ways:
- A global 404 page for the entire application
- Custom 404 pages for specific segments or dynamic parameters
Global 404 Page
To create a global 404 page in Next.js 16, you need to create a not-found.js or not-found.tsx file in the root app directory:
1// app/not-found.tsx
2import Link from 'next/link';
3
4export default function NotFound() {
5 return (
6 <div className="quantum-not-found">
7 <h1>404: Quantum Location Not Found</h1>
8 <div className="quantum-hologram">
9 <div className="hologram-effect">404</div>
10 </div>
11 <p>
12 We're sorry, but the place you're looking for doesn't exist in Metropolis Quantum.
13 Our quantum sensors could not locate the requested destination in any of the available
14 spacetime dimensions.
15 </p>
16 <p>
17 Possible causes of the error:
18 </p>
19 <ul>
20 <li>The quantum coordinates are incorrect</li>
21 <li>The location existed but has been removed</li>
22 <li>Quantum fluctuations temporarily disrupted access</li>
23 </ul>
24 <div className="action-buttons">
25 <Link href="/" className="quantum-button primary">
26 Return to Control Center
27 </Link>
28 <Link href="/map" className="quantum-button secondary">
29 Open Quantum Map
30 </Link>
31 </div>
32 </div>
33 );
34}This page will be displayed automatically when a user tries to access a non-existent URL path.
Custom 404 Pages for Specific Segments
Next.js 16 also allows creating custom 404 pages for specific application segments. You can create a not-found.js or not-found.tsx file in any directory, and it will be used for that specific segment and its sub-paths:
1// app/quantum-properties/not-found.tsx
2import Link from 'next/link';
3
4export default function QuantumPropertiesNotFound() {
5 return (
6 <div className="property-not-found">
7 <h1>Quantum Property Not Found</h1>
8 <p>
9 Unfortunately, we couldn't find the quantum property you're looking for.
10 Our sensors didn't detect quantum signatures at the specified location.
11 </p>
12 <div className="property-suggestions">
13 <h2>Check out our most popular properties:</h2>
14 <div className="suggestion-list">
15 {/* List of popular properties */}
16 </div>
17 </div>
18 <Link href="/quantum-properties" className="quantum-button">
19 Return to property search
20 </Link>
21 </div>
22 );
23}Triggering not-found from a Component
We can also programmatically trigger the display of a 404 page from any component or data loading function, using the notFound function from next/navigation:
1// app/quantum-properties/[id]/page.tsx
2import { notFound } from 'next/navigation';
3import { getPropertyById } from '@/lib/api';
4
5export default async function QuantumPropertyPage({ params }: { params: Promise<{ id: string }> }) {
6 const property = await getPropertyById((await params).id);
7
8 // If the property doesn't exist, display the 404 page
9 if (!property) {
10 notFound();
11 }
12
13 return (
14 <div className="quantum-property-details">
15 <h1>{property.name}</h1>
16 {/* Property details */}
17 </div>
18 );
19}This is particularly useful for dynamic pages where we want to check if a given resource exists, and if not - display the 404 page.
Generating Metadata for the 404 Page
We can also customize metadata for our 404 page to improve SEO and user experience:
1// app/not-found.tsx
2import type { Metadata } from 'next';
3import Link from 'next/link';
4
5export const metadata: Metadata = {
6 title: 'Page Not Found | Metropolis Quantum',
7 description: 'We could not find the page you are looking for in Metropolis Quantum.',
8};
9
10export default function NotFound() {
11 // 404 page content
12}Redirects in Next.js 16
In Metropolis Quantum, when an old location is moved or replaced, the transport system automatically redirects travelers to the new destination. In Next.js, we similarly use redirects to direct users from outdated URLs to new locations.
Next.js 16 offers several methods for implementing redirects:
- Static redirects in configuration
- Dynamic redirects in code
- Temporary vs. permanent redirects
Static Redirects in next.config.js
The simplest way to define redirects is using the next.config.js configuration file:
1// next.config.js
2module.exports = {
3 async redirects() {
4 return [
5 {
6 // Redirect from old path to new one
7 source: '/old-quantum-district',
8 destination: '/quantum-districts/central',
9 permanent: true, // status code 308 (permanent redirect)
10 },
11 {
12 // Redirect with parameters
13 source: '/quantum-labs/:labId',
14 destination: '/research-facilities/:labId',
15 permanent: false, // status code 307 (temporary redirect)
16 },
17 {
18 // Redirect preserving query parameters
19 source: '/search-old',
20 destination: '/search-new',
21 permanent: false,
22 },
23 {
24 // Redirect with multiple parameters and regex
25 source: '/quantum-zones/:zoneId(\\d{1,})',
26 destination: '/zones/:zoneId',
27 permanent: true,
28 },
29 ];
30 },
31};Each redirect contains:
source: the source URL pathdestination: the destination URL pathpermanent: a flag indicating whether the redirect is permanent or temporary
The difference between permanent and temporary redirects:
- Permanent (code 308): Informs browsers and search engine bots that the page has been permanently moved. Search engines remember this redirect and automatically navigate to the new URL in the future.
- Temporary (code 307): Indicates that the page is temporarily available at a different address but may return to the original URL. Search engines do not remember this redirect for long.
Programmatic Redirects in Code
Next.js 16 also enables dynamic redirects directly in components or data functions using the redirect function from next/navigation:
1// app/quantum-profile/[userId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getUserById } from '@/lib/api';
4
5export default async function UserProfilePage({ params }: { params: Promise<{ userId: string }> }) {
6 const user = await getUserById((await params).userId);
7
8 // If the user doesn't exist, redirect to login page
9 if (!user) {
10 redirect('/login');
11 }
12
13 // If the user is an admin, redirect to admin dashboard
14 if (user.role === 'admin') {
15 redirect('/admin/dashboard');
16 }
17
18 return (
19 <div className="user-profile">
20 <h1>Profil: {user.name}</h1>
21 {/* Profile content */}
22 </div>
23 );
24}The redirect function is particularly useful in situations where the redirect decision depends on dynamic data, such as user status, permissions, or resource availability.
Redirects in Proxy (formerly Middleware)
Proxy (middleware in Next.js 15 and earlier) allows intercepting requests before they are processed by the application, enabling the implementation of more complex redirect logic:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Get path from URL
7 const pathname = request.nextUrl.pathname;
8
9 // Conditional redirect based on domain
10 if (request.headers.get('host')?.includes('old-domain.com')) {
11 return NextResponse.redirect(
12 new URL(pathname, 'https://new-domain.com')
13 );
14 }
15
16 // Redirect old paths to new ones
17 if (pathname.startsWith('/legacy-api')) {
18 return NextResponse.redirect(
19 new URL(pathname.replace('/legacy-api', '/api/v2'), request.url)
20 );
21 }
22
23 // Redirect based on geolocation
24 // NextRequest no longer has a geo field (removed in Next.js 15) - the platform provides the country, e.g. Vercel in the x-vercel-ip-country header
25 const country = request.headers.get('x-vercel-ip-country') || 'US';
26 if (pathname === '/events' && country === 'PL') {
27 return NextResponse.redirect(new URL('/wydarzenia', request.url));
28 }
29
30 // Redirect based on browser language preferences
31 const preferredLocale = request.headers.get('accept-language')?.split(',')[0].split('-')[0];
32 if (pathname === '/' && preferredLocale === 'pl' && !pathname.includes('/pl')) {
33 return NextResponse.redirect(new URL('/pl', request.url));
34 }
35
36 return NextResponse.next();
37}
38
39export const config = {
40 matcher: [
41 '/((?!api|_next/static|_next/image|favicon.ico).*)',
42 ],
43};Middleware gives us enormous flexibility, enabling redirects based on:
- HTTP headers
- Cookies
- Geolocation
- User-Agent
- Query parameters
- And many other factors
Conditional Redirects in Server Components
In Server Components, we can use conditional redirects based on data fetched from the API or database:
1// app/quantum-experiment/[experimentId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getExperimentById, getUserPermissions } from '@/lib/api';
4
5export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
6 // Fetch experiment data
7 const experiment = await getExperimentById((await params).experimentId);
8
9 // If the experiment has been moved, redirect to new location
10 if (experiment?.movedTo) {
11 redirect(`/quantum-experiment/${experiment.movedTo}`);
12 }
13
14 // If the experiment is private, check user permissions
15 if (experiment?.isPrivate) {
16 const userPermissions = await getUserPermissions();
17
18 if (!userPermissions.canAccessPrivateExperiments) {
19 redirect('/access-denied');
20 }
21 }
22
23 return (
24 <div className="experiment-details">
25 <h1>{experiment.title}</h1>
26 {/* Experiment details */}
27 </div>
28 );
29}Redirects in Client Components
In client components, we can also perform redirects using the useRouter hook:
1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useEffect } from 'react';
5import { useAuth } from '@/lib/auth';
6
7export default function ProtectedPage() {
8 const router = useRouter();
9 const { user, loading } = useAuth();
10
11 useEffect(() => {
12 // Redirect unauthenticated users to login page
13 if (!loading && !user) {
14 router.push('/login');
15 }
16 }, [user, loading, router]);
17
18 if (loading) {
19 return <div>Loading...</div>;
20 }
21
22 if (!user) {
23 return null; // Don't render anything during redirect
24 }
25
26 return (
27 <div className="protected-content">
28 <h1>Welcome, {user.name}!</h1>
29 {/* Protected page content */}
30 </div>
31 );
32}Advanced 404 and Redirect Handling Techniques
Handling Non-Existent Dynamic Parameters
For dynamic routes with parameters, we can generate static pages only for specific parameters, and display a 404 page for the rest:
1// app/quantum-experiments/[experimentId]/page.tsx
2import { notFound } from 'next/navigation';
3import { getExperimentById, getAllExperimentIds } from '@/lib/api';
4
5// Generate static pages for known experiments
6export async function generateStaticParams() {
7 const experimentIds = await getAllExperimentIds();
8
9 return experimentIds.map((id) => ({
10 experimentId: id,
11 }));
12}
13
14export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
15 const experiment = await getExperimentById((await params).experimentId);
16
17 // If the experiment doesn't exist, display 404
18 if (!experiment) {
19 notFound();
20 }
21
22 return (
23 <div className="experiment-details">
24 <h1>{experiment.title}</h1>
25 {/* Experiment details */}
26 </div>
27 );
28}Handling Expired Content
Sometimes content may be temporarily available or expire after a certain time. We can handle such cases by automatically redirecting to alternative pages:
1// app/quantum-events/[eventId]/page.tsx
2import { redirect, notFound } from 'next/navigation';
3import { getEventById } from '@/lib/api';
4
5export default async function EventPage({ params }: { params: Promise<{ eventId: string }> }) {
6 const event = await getEventById((await params).eventId);
7
8 // If the event doesn't exist, display 404
9 if (!event) {
10 notFound();
11 }
12
13 // Check if the event has already ended
14 const now = new Date();
15 const eventEndDate = new Date(event.endDate);
16
17 if (now > eventEndDate) {
18 // If recordings are available, redirect to them
19 if (event.recordings) {
20 redirect(`/quantum-events/recordings/${(await params).eventId}`);
21 }
22
23 // If there are future events planned in this series, redirect to them
24 if (event.series && event.nextInSeries) {
25 redirect(`/quantum-events/${event.nextInSeries}`);
26 }
27
28 // Otherwise redirect to the archive page
29 redirect('/quantum-events/archive');
30 }
31
32 return (
33 <div className="event-details">
34 <h1>{event.title}</h1>
35 {/* Event details */}
36 </div>
37 );
38}Custom A/B Test-Based Redirects
We can also implement redirects based on A/B tests to direct users to different page versions:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Helper function to select A/B test variant
6function getABTestVariant(userId: string, testName: string, variants: string[]): string {
7 // We use a deterministic algorithm based on userId and test name
8 const hash = hashString(`${userId}-${testName}`);
9 const variantIndex = hash % variants.length;
10 return variants[variantIndex];
11}
12
13// Helper function to generate hash from string
14function hashString(str: string): number {
15 let hash = 0;
16 for (let i = 0; i < str.length; i++) {
17 hash = ((hash << 5) - hash) + str.charCodeAt(i);
18 hash |= 0; // Convert to 32-bit integer
19 }
20 return Math.abs(hash);
21}
22
23export function proxy(request: NextRequest) {
24 const pathname = request.nextUrl.pathname;
25
26 // A/B test for the homepage
27 if (pathname === '/') {
28 // Get or generate user ID
29 let userId = request.cookies.get('user_id')?.value;
30
31 if (!userId) {
32 userId = crypto.randomUUID();
33 // In a real implementation, this cookie should be saved in the response
34 }
35
36 // Determine A/B test variant for this user
37 const homePageVariant = getABTestVariant(userId, 'homepage_redesign', ['control', 'variant_a', 'variant_b']);
38
39 // Redirect to the appropriate variant
40 if (homePageVariant !== 'control') {
41 return NextResponse.redirect(new URL(`/home-${homePageVariant}`, request.url));
42 }
43 }
44
45 return NextResponse.next();
46}
47
48export const config = {
49 matcher: ['/'],
50};API Status-Based Redirects
We can perform redirects based on API responses:
1// app/quantum-dashboard/page.tsx
2import { redirect } from 'next/navigation';
3import { getSystemStatus } from '@/lib/api';
4
5export default async function DashboardPage() {
6 let status;
7 try {
8 status = await getSystemStatus();
9 } catch (error) {
10 // If the API is unavailable, redirect to the status page
11 redirect('/system-status');
12 }
13
14 // redirect() throws a special error, so we call it outside try/catch
15 if (status.maintenance) {
16 redirect('/maintenance');
17 }
18 if (status.requiresClientUpdate) {
19 redirect('/update-required');
20 }
21
22 return (
23 <div className="quantum-dashboard">
24 <h1>Control Panel</h1>
25 {/* Panel content */}
26 </div>
27 );
28}Analytics System Integration
For redirects and 404 pages, it's worth tracking which paths lead to errors or redirects to optimize user experience. Here is an example of implementing tracking for 404 pages:
1// app/not-found.tsx
2'use client';
3
4import { useEffect } from 'react';
5import Link from 'next/link';
6import { usePathname } from 'next/navigation';
7import { trackEvent } from '@/lib/analytics';
8
9export default function NotFound() {
10 const pathname = usePathname();
11
12 useEffect(() => {
13 // Track 404 event
14 trackEvent('404_error', {
15 path: pathname,
16 timestamp: new Date().toISOString(),
17 referrer: document.referrer
18 });
19 }, [pathname]);
20
21 return (
22 <div className="quantum-not-found">
23 <h1>404: Quantum Location Not Found</h1>
24 {/* 404 page content */}
25 </div>
26 );
27}Testing Redirects and 404 Pages
It's important to thoroughly test redirects and 404 pages to ensure they work as expected:
1// tests/redirects.test.js
2import { createServer } from 'http';
3import { parse } from 'url';
4import fetch from 'node-fetch';
5import { NextRequest } from 'next/server';
6import { proxy } from '../proxy';
7
8describe('Redirects Tests', () => {
9 let server;
10 let port;
11
12 beforeAll(() => {
13 // Start local test server
14 server = createServer((req, res) => {
15 res.writeHead(200).end('Test server');
16 }).listen(0);
17 port = server.address().port;
18 });
19
20 afterAll(() => {
21 server.close();
22 });
23
24 test('Should redirect /legacy-api to /api/v2', async () => {
25 const req = new NextRequest(new URL(`http://localhost:${port}/legacy-api/users`));
26 const res = proxy(req);
27
28 expect(res.status).toBe(307);
29 expect(res.headers.get('Location')).toBe(`http://localhost:${port}/api/v2/users`);
30 });
31
32 test('Should handle country-specific redirects', async () => {
33 const req = new NextRequest(new URL(`http://localhost:${port}/events`), {
34 headers: { 'x-vercel-ip-country': 'PL' },
35 });
36
37 const res = proxy(req);
38
39 expect(res.status).toBe(307);
40 expect(res.headers.get('Location')).toBe(`http://localhost:${port}/wydarzenia`);
41 });
42});Summary
Proper handling of 404 pages and redirects is a crucial element of every professional Next.js application. Just as in Metropolis Quantum, where advanced navigation systems ensure that every traveler reaches the right place (even if the original destination doesn't exist), we must also ensure a smooth user experience in our application, even when encountering errors.
Next.js 16 offers comprehensive tools for implementing:
- Custom global and segment-specific 404 pages
- Static redirects in configuration
- Dynamic redirects in code
- Advanced redirect rules in the proxy
With these tools, we can create intuitive and user-friendly applications that elegantly handle even unexpected situations.
In the next lesson you will learn the proxy (formerly middleware), which intercepts requests before they reach the page.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What happens to the layout.tsx component when navigating between pages that share the same layout?
2. How does template.tsx differ from layout.tsx in Next.js App Router?
3. The not-found.js file in App Router is displayed:
4. Intercepting Routes in App Router allow for:
Hands-on tasks in the game
- Code editor
Create /app/dashboard/page.tsx: export default function Dashboard() { return <main className='dashboard-grid'><h1>Dashboard</h1><div className='lo-panel'>System Online</div></main> }. You will see the result in the preview next to the editor.
- Horizontal ordering
Arrange the Metadata type import from the Next.js library
- Code editor
Create /app/login/page.tsx with a form: 'use client' at the very top (useState is needed), export default function LoginPage(), the state const [email, setEmail] = useState(''), inside the form <input value={email} onChange={e => setEmail(e.target.value)} /> and a βLog in to the networkβ button. Add basic styles!
- Code editor
Create an async Server Component (default export): export default async function ImplantsDashboard() { const implants = await fetch('https://api.example.com/implants').then(r => r.json()); return <ul>{implants.map(i => <li key={i.id}>{i.name}</li>)}</ul> }. On the server, fetch needs a full URL. No 'use client' = Server Component!
- Vertical ordering
Arrange the component rendering order in App Router
- Click in order
Arrange the metadata object declaration syntax in Next.js
- Code editor
In the terminal, create a structure with the Route Group (marketing) containing the about and contact pages: mkdir -p creates the folders app/(marketing)/about and app/(marketing)/contact, and touch adds a page.tsx file to each of them. Put paths with parentheses in quotes, e.g. mkdir -p "app/(marketing)/about".