Pollito Dev
November 8, 2025

Large Software Projects: Handling Errors

Posted on November 8, 2025  •  5 minutes  • 1050 words  • Other languages:  Español

This post is part of my Large Software Projects blog series .

Code Source

All code snippets shown in this post are available in the dedicated branch for this article on the project’s GitHub repository. Feel free to clone it and follow along:

https://github.com/franBec/tas/tree/feature/2025-11-08

Why Error Handling Matters

A large software project must allow the user to recover gracefully when something inevitably goes wrong.

Next.js provides component-based patterns for managing both unexpected runtime errors and 404-page failures.

Segment Error Boundary

In the App Router, the error.tsx file defines a React Error Boundary specific to a route segment and its nested children.

Here is an implementation for the top-level application error boundary:

"use client";

import { startTransition } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { AlertTriangle } from "lucide-react";

import { PageLayout } from "@/components/layout/page-layout";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

interface NextJsError extends Error {
  digest?: string;
}

const ErrorBoundary = ({
  error,
  reset,
}: {
  error: NextJsError;
  reset: () => void;
}) => {
  const router = useRouter();
  
  // Use startTransition to keep the UI responsive during the refresh operation
  const reload = () => {
    startTransition(() => {
      router.refresh();
      reset();
    });
  };
  return (
    <PageLayout>
      <PageLayout.TwoColumn>
        <PageLayout.LeftColumn>
          <Card>
            <CardHeader>
              <CardTitle className="flex items-center gap-2">
                <AlertTriangle />
                Something went wrong
              </CardTitle>
              <CardDescription>
                We are sorry, but something unexpected happened.
              </CardDescription>
            </CardHeader>
            <CardContent>
              <p className="text-destructive">
                {/* The digest is useful for looking up the error in server logs */}
                {error.digest ? `Error Reference: ${error.digest}` : null}
              </p>
            </CardContent>
            <CardFooter>
              <Button onClick={reload}>Try Again</Button>
            </CardFooter>
          </Card>
        </PageLayout.LeftColumn>
        <PageLayout.RightColumn>
          <Image
            src="/undraw_connection-lost_am29.svg"
            alt="Connection lost illustration"
            width={600}
            height={400}
            className="w-full max-w-lg"
          />
        </PageLayout.RightColumn>
      </PageLayout.TwoColumn>
    </PageLayout>
  );
};
export default ErrorBoundary;

Error boundary

Global Error Boundary

While error.tsx catches errors within a segment, it cannot catch errors thrown in the parent layout.tsx file for the same segment. This is because the layout component sits higher in the React component tree than the boundary itself.

For catching critical errors thrown in the root src/app/layout.tsx, Next.js provides the special file src/app/global-error.tsx.

Here is an implementation:

"use client";

import Image from "next/image";
import { AlertTriangle } from "lucide-react";

import { PageLayout } from "@/components/layout/page-layout";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

interface NextJsError extends Error {
  digest?: string;
}

const GlobalError = ({ error }: { error: NextJsError }) => {
  return (
    <html>
      <body>
        <PageLayout>
          <PageLayout.Header
            title="Municipal Services"
            subtitle="Your Digital Gateway to Local Government Services"
          />
          <PageLayout.TwoColumn>
            <PageLayout.LeftColumn>
              <Card>
                <CardHeader>
                  <CardTitle className="flex items-center gap-2">
                    <AlertTriangle />
                    Something went wrong
                  </CardTitle>
                  <CardDescription>
                    We are sorry, but something unexpected happened.
                  </CardDescription>
                </CardHeader>
                <CardContent>
                  <p className="text-destructive">
                    {error.digest ? `Error Reference: ${error.digest}` : null}
                  </p>
                </CardContent>
                <CardFooter className="flex gap-4">
                  <Button variant="secondary" asChild>
                    <a href="/">Go to Home</a>
                  </Button>
                </CardFooter>
              </Card>
            </PageLayout.LeftColumn>
            <PageLayout.RightColumn>
              <Image
                src="/undraw_connection-lost_am29.svg"
                alt="Connection lost illustration"
                width={600}
                height={400}
                className="w-full max-w-lg"
              />
            </PageLayout.RightColumn>
          </PageLayout.TwoColumn>
        </PageLayout>
      </body>
    </html>
  );
};
export default GlobalError;

Note on Navigation

Because the root application state is completely broken in a global error scenario, attempting a soft navigation (using Next.js <Link>) is unreliable. We use a standard hard refresh link (<a href="/">) to force a full application reset.

Since this use of <a> instead of <Link> might conflict with standard linting rules, we must add src/app/global-error.test.tsx to the ignores list of eslint.config.mjs.

Testing the Global Boundary

Due to the fundamental differences between React Error Boundaries in development and production, global-error.tsx only activates when running a production build (next build && next start).

To verify your implementation, you could temporarily introduce a simple button that explicitly throws an error inside src/app/layout.tsx, create a production build, and run the built application.

button that throws error

global boundary screen

Not Found Page

The not-found.tsx file handles 404 errors when a user navigates to a URL that does not match any defined route. This page is triggered either automatically by Next.js when no routes match the URL, or explicitly by calling the notFound() utility function within server components.

Here is our custom src/app/not-found.tsx:

"use client";

import Image from "next/image";
import Link from "next/link";
import { SearchX } from "lucide-react";

import { PageLayout } from "@/components/layout/page-layout";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

const NotFound = () => {
  return (
    <PageLayout>
      <PageLayout.TwoColumn>
        <PageLayout.LeftColumn>
          <Card>
            <CardHeader>
              <CardTitle className="flex items-center gap-2">
                <SearchX />
                Page Not Found
              </CardTitle>
              <CardDescription>
                We are sorry, but the page you are looking for does not exist.
              </CardDescription>
            </CardHeader>
            <CardContent>
              <p>
                The link you followed may be broken, or the page may have been
                removed.
              </p>
            </CardContent>
            <CardFooter>
              <Button asChild>
                <Link href="/">Go to Homepage</Link>
              </Button>
            </CardFooter>
          </Card>
        </PageLayout.LeftColumn>
        <PageLayout.RightColumn>
          <Image
            src="/undraw_void_wez2.svg"
            alt="Page not found illustration"
            width={600}
            height={400}
            className="w-full max-w-lg"
          />
        </PageLayout.RightColumn>
      </PageLayout.TwoColumn>
    </PageLayout>
  );
};

export default NotFound;

Not Found Page

Next Steps

We took a moment to shore up the user experience before diving deep into complex features. We can now tackle the next crucial and complicated architectural challenge: Authentication and Authorization.

Hey, check me out!

You can find me here