Skip to main content
Back to Blog
Tutorials
3 min read
December 10, 2024

How to Implement Error Boundaries and Error Recovery in React

Build comprehensive error boundaries in React with recovery mechanisms, error reporting, and graceful degradation patterns.

Ryel Banfield

Founder & Lead Developer

Error boundaries prevent your entire app from crashing when a component throws. Here is how to build robust error handling with recovery options.

Basic Error Boundary

"use client";

import { Component, type ErrorInfo, type ReactNode } from "react";

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  reset = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError && this.state.error) {
      if (typeof this.props.fallback === "function") {
        return this.props.fallback(this.state.error, this.reset);
      }
      if (this.props.fallback) {
        return this.props.fallback;
      }
      return <DefaultErrorFallback error={this.state.error} reset={this.reset} />;
    }

    return this.props.children;
  }
}

Default Error Fallback Component

interface ErrorFallbackProps {
  error: Error;
  reset: () => void;
}

function DefaultErrorFallback({ error, reset }: ErrorFallbackProps) {
  return (
    <div
      role="alert"
      className="rounded-lg border border-red-200 bg-red-50 p-6 text-center"
    >
      <div className="mx-auto mb-4 h-12 w-12 rounded-full bg-red-100 flex items-center justify-center">
        <svg
          className="h-6 w-6 text-red-600"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
          aria-hidden="true"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z"
          />
        </svg>
      </div>
      <h2 className="text-lg font-semibold text-red-800 mb-2">
        Something went wrong
      </h2>
      <p className="text-sm text-red-600 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-red-600 text-white rounded-md text-sm hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  );
}

Next.js error.tsx for Route Segments

Next.js has built-in error boundary support with error.tsx.

// app/(site)/dashboard/error.tsx
"use client";

interface ErrorPageProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function DashboardError({ error, reset }: ErrorPageProps) {
  return (
    <div className="flex min-h-[400px] items-center justify-center">
      <div className="text-center">
        <h2 className="text-2xl font-bold mb-2">Dashboard Error</h2>
        <p className="text-muted-foreground mb-4">
          {error.message || "An unexpected error occurred"}
        </p>
        {error.digest && (
          <p className="text-xs text-muted-foreground mb-4">
            Error ID: {error.digest}
          </p>
        )}
        <div className="flex gap-2 justify-center">
          <button
            onClick={reset}
            className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
          >
            Retry
          </button>
          <a href="/" className="px-4 py-2 border rounded-md">
            Go Home
          </a>
        </div>
      </div>
    </div>
  );
}

Error Boundary with Retry Logic

"use client";

import { ErrorBoundary } from "./ErrorBoundary";
import { type ReactNode, useCallback, useState } from "react";

interface RetryBoundaryProps {
  children: ReactNode;
  maxRetries?: number;
}

export function RetryBoundary({ children, maxRetries = 3 }: RetryBoundaryProps) {
  const [retryCount, setRetryCount] = useState(0);
  const [key, setKey] = useState(0);

  const handleError = useCallback(() => {
    // Error logging could go here
  }, []);

  const handleRetry = useCallback(() => {
    if (retryCount < maxRetries) {
      setRetryCount((prev) => prev + 1);
      setKey((prev) => prev + 1); // Force remount of children
    }
  }, [retryCount, maxRetries]);

  return (
    <ErrorBoundary
      key={key}
      onError={handleError}
      fallback={(error, reset) => (
        <div role="alert" className="p-6 border rounded-lg text-center">
          <p className="font-medium mb-2">Something went wrong</p>
          <p className="text-sm text-muted-foreground mb-4">{error.message}</p>
          {retryCount < maxRetries ? (
            <button
              onClick={() => {
                handleRetry();
                reset();
              }}
              className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm"
            >
              Retry ({retryCount}/{maxRetries})
            </button>
          ) : (
            <div>
              <p className="text-sm text-red-500 mb-2">Maximum retries exceeded</p>
              <a href="/" className="text-sm text-primary hover:underline">
                Return home
              </a>
            </div>
          )}
        </div>
      )}
    >
      {children}
    </ErrorBoundary>
  );
}

Granular Error Boundaries

Wrap specific sections to prevent cascade failures.

// app/(site)/dashboard/page.tsx
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      <ErrorBoundary
        fallback={
          <div className="p-4 border rounded-lg text-center text-muted-foreground">
            Revenue chart unavailable
          </div>
        }
      >
        <Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
          <RevenueChart />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary
        fallback={
          <div className="p-4 border rounded-lg text-center text-muted-foreground">
            Activity feed unavailable
          </div>
        }
      >
        <Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
          <ActivityFeed />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary>
        <Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
          <UserTable />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary>
        <Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
          <Notifications />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Error Reporting Integration

// lib/error-reporting.ts
interface ErrorReport {
  message: string;
  stack?: string;
  componentStack?: string;
  url: string;
  timestamp: string;
  userAgent: string;
}

export function reportError(error: Error, componentStack?: string) {
  const report: ErrorReport = {
    message: error.message,
    stack: error.stack,
    componentStack,
    url: typeof window !== "undefined" ? window.location.href : "",
    timestamp: new Date().toISOString(),
    userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
  };

  // Send to your error tracking service
  if (process.env.NODE_ENV === "production") {
    fetch("/api/errors", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(report),
    }).catch(() => {
      // Silently fail — don't throw errors in error handling
    });
  } else {
    console.error("Error report:", report);
  }
}

Wire it up:

<ErrorBoundary
  onError={(error, errorInfo) => {
    reportError(error, errorInfo.componentStack ?? undefined);
  }}
>
  <App />
</ErrorBoundary>

Async Error Handling Pattern

Error boundaries only catch render errors. For async operations:

"use client";

import { useState, useCallback } from "react";

export function useAsyncAction<T>(
  action: () => Promise<T>
): {
  execute: () => Promise<T | undefined>;
  loading: boolean;
  error: Error | null;
  reset: () => void;
} {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const execute = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await action();
      return result;
    } catch (e) {
      const err = e instanceof Error ? e : new Error("Unknown error");
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [action]);

  const reset = useCallback(() => setError(null), []);

  return { execute, loading, error, reset };
}

Need Resilient Web Applications?

We build production-grade applications with comprehensive error handling, monitoring, and graceful degradation. Contact us to build reliable software.

error boundarieserror handlingReactresiliencetutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles