Back to Blog

React Error Boundaries in 2026: Suspense Fallbacks, Error Recovery, and Sentry Integration

Master React error boundaries in 2026: class vs react-error-boundary, Suspense composition, granular error recovery, Sentry integration, and error boundary patterns for Next.js App Router.

Viprasol Tech Team
January 29, 2027
13 min read

React Error Boundaries in 2026: Suspense Fallbacks, Error Recovery, and Sentry Integration

JavaScript errors inside component trees used to crash the entire React app. Error boundaries catch these errors, render a fallback UI, and let the rest of the app keep working. Used correctly alongside Suspense, they create resilient UIs where a broken widget doesn't take down the whole dashboard.

This post covers the full error boundary toolkit: when to use the class component approach vs react-error-boundary, granular placement strategy (one per widget, not one per app), composing error boundaries with Suspense, retry/reset patterns, and Sentry integration for production error tracking.


The Problem Error Boundaries Solve

// Without error boundary β€” one broken component crashes the page
function Dashboard() {
  return (
    <div>
      <Header />
      <RevenueChart />      {/* If this throws, nothing renders */}
      <ActivityFeed />
      <TaskList />
    </div>
  );
}

// With granular error boundaries β€” broken widget is isolated
function Dashboard() {
  return (
    <div>
      <Header />
      <ErrorBoundary fallback={<ChartError />}>
        <RevenueChart />    {/* Throws β†’ shows ChartError, rest of dashboard works */}
      </ErrorBoundary>
      <ErrorBoundary fallback={<FeedError />}>
        <ActivityFeed />
      </ErrorBoundary>
      <TaskList />
    </div>
  );
}

react-error-boundary β€” The Right Library

Don't write class components for error boundaries. Use react-error-boundary:

npm install react-error-boundary
// components/ErrorBoundary/ErrorBoundary.tsx
"use client";

import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary";
import { ReactNode, useCallback } from "react";
import * as Sentry from "@sentry/nextjs";

// ── Default error fallback ────────────────────────────────────────────────────

function DefaultFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div
      role="alert"
      className="rounded-lg border border-red-200 bg-red-50 p-4 text-center"
    >
      <p className="text-sm font-medium text-red-800">Something went wrong</p>
      <p className="text-xs text-red-600 mt-1 font-mono">
        {error instanceof Error ? error.message : "Unknown error"}
      </p>
      <button
        onClick={resetErrorBoundary}
        className="mt-3 px-3 py-1.5 bg-red-600 text-white rounded text-xs font-medium hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  );
}

// ── Wrapper with Sentry reporting ─────────────────────────────────────────────

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode | ((props: FallbackProps) => ReactNode);
  onReset?: () => void;
  name?: string;  // For Sentry context
}

export function ErrorBoundary({
  children,
  fallback,
  onReset,
  name = "unknown",
}: ErrorBoundaryProps) {
  const handleError = useCallback(
    (error: Error, info: { componentStack: string }) => {
      // Report to Sentry with component context
      Sentry.withScope((scope) => {
        scope.setTag("error_boundary", name);
        scope.setExtra("componentStack", info.componentStack);
        Sentry.captureException(error);
      });

      // Also log to console in development
      if (process.env.NODE_ENV === "development") {
        console.error(`[ErrorBoundary: ${name}]`, error, info);
      }
    },
    [name]
  );

  const FallbackComponent =
    typeof fallback === "function"
      ? fallback
      : fallback !== undefined
      ? () => <>{fallback}</>
      : DefaultFallback;

  return (
    <ReactErrorBoundary
      FallbackComponent={FallbackComponent}
      onError={handleError}
      onReset={onReset}
    >
      {children}
    </ReactErrorBoundary>
  );
}

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different β€” senior engineers only, direct Slack access, and a 5.0β˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript β€” production-grade stack
  • Fixed-price contracts β€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Composing with Suspense

Error boundaries and Suspense are designed to work together. Suspense handles the loading state; error boundaries handle failures:

// components/AsyncWidget/AsyncWidget.tsx
"use client";

import { Suspense } from "react";
import { ErrorBoundary } from "@/components/ErrorBoundary";

// Loading skeleton
function WidgetSkeleton() {
  return (
    <div className="animate-pulse rounded-lg bg-gray-100 h-48" />
  );
}

// Error state
function WidgetError({ error, resetErrorBoundary }: any) {
  return (
    <div role="alert" className="rounded-lg border border-red-100 bg-red-50 p-4 text-sm">
      <p className="font-medium text-red-700">Failed to load widget</p>
      <button
        onClick={resetErrorBoundary}
        className="mt-2 text-xs text-red-600 underline hover:no-underline"
      >
        Retry
      </button>
    </div>
  );
}

// Usage pattern: ErrorBoundary wraps Suspense wraps async component
export function AsyncWidget({ children, name }: { children: React.ReactNode; name: string }) {
  return (
    <ErrorBoundary fallback={WidgetError} name={name}>
      <Suspense fallback={<WidgetSkeleton />}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// Dashboard using the pattern
export function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      <AsyncWidget name="revenue-chart">
        <RevenueChart />
      </AsyncWidget>

      <AsyncWidget name="activity-feed">
        <ActivityFeed />
      </AsyncWidget>

      <AsyncWidget name="task-summary">
        <TaskSummary />
      </AsyncWidget>
    </div>
  );
}

useErrorBoundary Hook for Imperative Throwing

Catch async errors (like fetch failures in event handlers) and route them to the nearest error boundary:

// components/DataTable/DataTable.tsx
"use client";

import { useState } from "react";
import { useErrorBoundary } from "react-error-boundary";

export function DataTable({ entityId }: { entityId: string }) {
  const [isDeleting, setIsDeleting] = useState(false);
  const { showBoundary } = useErrorBoundary();

  const handleDelete = async (rowId: string) => {
    setIsDeleting(true);
    try {
      const res = await fetch(`/api/data/${rowId}`, { method: "DELETE" });
      if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
      // Success β€” update local state
    } catch (err) {
      // Route the async error to the nearest ErrorBoundary
      showBoundary(err);
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <table>
      {/* ... */}
    </table>
  );
}

πŸš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Next.js App Router Error Files

Next.js App Router has built-in error boundary support via error.tsx and global-error.tsx:

// app/dashboard/error.tsx
// Automatically wraps the /dashboard route segment as an error boundary
"use client";

import { useEffect } from "react";
import * as Sentry from "@sentry/nextjs";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Report to Sentry
    Sentry.captureException(error, {
      tags: { route: "/dashboard" },
      extra: { digest: error.digest },
    });
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
      <div className="text-center">
        <h2 className="text-xl font-semibold text-gray-900">Dashboard unavailable</h2>
        <p className="text-gray-500 mt-1 text-sm">
          We're having trouble loading your dashboard.
        </p>
        {error.digest && (
          <p className="text-xs text-gray-400 mt-2 font-mono">
            Error ID: {error.digest}
          </p>
        )}
      </div>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
      >
        Reload dashboard
      </button>
    </div>
  );
}
// app/global-error.tsx
// Catches errors in the root layout β€” renders without the root layout
"use client";

import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <html>
      <body>
        <div className="flex min-h-screen items-center justify-center p-4">
          <div className="text-center max-w-sm">
            <h1 className="text-2xl font-bold text-gray-900">
              Something went wrong
            </h1>
            <p className="text-gray-500 mt-2">
              An unexpected error occurred. Our team has been notified.
            </p>
            <div className="flex gap-3 mt-6 justify-center">
              <button
                onClick={reset}
                className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm"
              >
                Try again
              </button>
              <a
                href="/"
                className="px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-700"
              >
                Go home
              </a>
            </div>
          </div>
        </div>
      </body>
    </html>
  );
}

Sentry Integration

// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NODE_ENV,

  // Only sample 20% of errors in production to control volume
  sampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0,

  // Attach user context automatically
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration({
      // Session replay for 10% of sessions (useful for reproducing UI bugs)
      sessionSampleRate: 0.1,
      errorSampleRate: 1.0,  // Always replay sessions with errors
    }),
  ],

  // Ignore known non-actionable errors
  ignoreErrors: [
    "ResizeObserver loop limit exceeded",
    "Network request failed",
    /^Loading chunk \d+ failed/,
    "ChunkLoadError",
  ],
});
// Identify user in Sentry (call after login)
import * as Sentry from "@sentry/nextjs";

export function identifySentryUser(user: { id: string; email: string; name: string }) {
  Sentry.setUser({
    id: user.id,
    email: user.email,
    username: user.name,
  });
}

// Clear on logout
export function clearSentryUser() {
  Sentry.setUser(null);
}

Error Boundary Placement Strategy

App Root
β”œβ”€β”€ GlobalError (Next.js global-error.tsx) β€” catches root layout errors
└── Layout
    β”œβ”€β”€ Sidebar ErrorBoundary β€” sidebar errors don't break main content
    └── Main Content
        β”œβ”€β”€ Page ErrorBoundary (Next.js error.tsx per route segment)
        └── Widget-level ErrorBoundaries
            β”œβ”€β”€ <AsyncWidget name="chart"> β†’ isolated chart failure
            β”œβ”€β”€ <AsyncWidget name="feed"> β†’ isolated feed failure
            └── <AsyncWidget name="stats"> β†’ isolated stats failure

Rule: Place error boundaries at the granularity where a failure is recoverable independently. One boundary per logical "widget" is usually right. Don't wrap every single componentβ€”too granular makes fallback UIs confusing.


Testing Error Boundaries

// components/ErrorBoundary/ErrorBoundary.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ErrorBoundary } from "./ErrorBoundary";

// Component that throws on demand
function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) throw new Error("Test error");
  return <div>Content</div>;
}

describe("ErrorBoundary", () => {
  const user = userEvent.setup();

  // Suppress console.error for expected throws
  beforeEach(() => {
    vi.spyOn(console, "error").mockImplementation(() => {});
  });
  afterEach(() => {
    vi.mocked(console.error).mockRestore();
  });

  it("renders children when no error", () => {
    render(
      <ErrorBoundary>
        <ThrowingComponent shouldThrow={false} />
      </ErrorBoundary>
    );
    expect(screen.getByText("Content")).toBeInTheDocument();
  });

  it("renders fallback when child throws", () => {
    render(
      <ErrorBoundary>
        <ThrowingComponent shouldThrow={true} />
      </ErrorBoundary>
    );
    expect(screen.getByRole("alert")).toBeInTheDocument();
    expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
  });

  it("recovers after reset", async () => {
    const { rerender } = render(
      <ErrorBoundary>
        <ThrowingComponent shouldThrow={true} />
      </ErrorBoundary>
    );

    expect(screen.getByRole("alert")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: /try again/i }));

    rerender(
      <ErrorBoundary>
        <ThrowingComponent shouldThrow={false} />
      </ErrorBoundary>
    );

    expect(screen.getByText("Content")).toBeInTheDocument();
  });
});

Cost and Timeline

TaskTimelineCost (USD)
react-error-boundary setup + base component0.5 day$300–$500
Sentry integration + user context0.5 day$300–$500
Next.js error.tsx per route segment0.5 day$300–$500
AsyncWidget Suspense + ErrorBoundary composition1 day$600–$1,000
Error boundary test suite0.5 day$300–$500
Full error boundary implementation3–4 days$2,500–$4,000

See Also


Working With Viprasol

We implement resilient React architectures with comprehensive error handling for production SaaS applications β€” from error boundary placement strategy through Sentry integration and session replay. Our team has retrofitted error boundary systems into codebases that were crashing entire pages on single component failures.

What we deliver:

  • Error boundary component library with Sentry integration
  • Next.js error.tsx for each route segment
  • Suspense + ErrorBoundary composition patterns
  • Error recovery (retry) flows
  • Testing for expected error states

Explore our web development services or contact us to add resilience to your React application.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms β€” we build it all with Next.js and React.

Free consultation β€’ No commitment β€’ Response within 24 hours

Viprasol Β· Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse β‰₯90 scores, mobile-first design, and full source code ownership. Senior engineers only β€” from architecture through deployment.