mcpbeat

Error Boundary

vibeeval/error-boundary

React error boundary hierarchy, fallback UI patterns, offline-first fallback, retry mechanisms, and graceful degradation.

5k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
521
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/vibeeval/vibecosystem --skill error-boundary

The instruction itself

22 sections, as written by the author

Error Boundary Patterns

React error boundary hierarchy and graceful degradation patterns.

Error Boundary Component

// Class-based (React requirement for componentDidCatch)
interface ErrorBoundaryProps {
  children: React.ReactNode
  fallback?: React.ComponentType<FallbackProps>
  onError?: (error: Error, info: React.ErrorInfo) => void
}

interface ErrorBoundaryState {
  error: Error | null
}

export interface FallbackProps {
  error: Error
  reset: () => void
}

export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { error: null }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { error }
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('[ErrorBoundary]', error, info.componentStack)
    this.props.onError?.(error, info)
  }

  reset = () => this.setState({ error: null })

  render() {
    if (this.state.error) {
      const Fallback = this.props.fallback ?? DefaultFallback
      return <Fallback error={this.state.error} reset={this.reset} />
    }
    return this.props.children
  }
}

// react-error-boundary library (recommended — battle-tested)
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'

<ErrorBoundary
  FallbackComponent={MyFallback}
  onError={(error, info) => reportToSentry(error, info)}
  onReset={() => queryClient.clear()}
>
  <App />
</ErrorBoundary>

Error Boundary Hierarchy

App (top-level — catches everything, shows full-page error)
├── Navigation (no boundary — nav errors bubble to app)
├── <ErrorBoundary fallback={PageError}>           ← page level
│   └── DashboardPage
│       ├── <ErrorBoundary fallback={SectionError}>  ← section level
│       │   └── StatsSection
│       └── <ErrorBoundary fallback={WidgetError}>   ← widget level
│           └── ChartWidget                           (one fails, others work)
└── Sidebar (separate boundary — sidebar error ≠ main content error)
// Page-level boundary (reset on navigation)
import { usePathname } from 'next/navigation'
import { ErrorBoundary } from 'react-error-boundary'

export function PageErrorBoundary({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <ErrorBoundary
      key={pathname}              // reset boundary on route change
      FallbackComponent={PageErrorFallback}
      onError={reportError}
    >
      {children}
    </ErrorBoundary>
  )
}

// Widget-level boundary (isolated, inline fallback)
export function WidgetErrorBoundary({ children, name }: { children: React.ReactNode; name: string }) {
  return (
    <ErrorBoundary
      fallback={<WidgetErrorFallback name={name} />}
      onError={(err) => reportError(err, { widget: name })}
    >
      {children}
    </ErrorBoundary>
  )
}

Fallback UI Design Patterns

// Full-page fallback (app boundary)
function PageErrorFallback({ error, reset }: FallbackProps) {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-4 p-8 text-center">
      <h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
      <p className="max-w-md text-gray-500">
        {process.env.NODE_ENV === 'development'
          ? error.message
          : 'An unexpected error occurred. Our team has been notified.'}
      </p>
      <div className="flex gap-3">
        <button onClick={reset} className="btn btn-primary">Try again</button>
        <a href="/" className="btn btn-outline">Go home</a>
      </div>
      {process.env.NODE_ENV === 'development' && (
        <pre className="mt-4 max-w-2xl overflow-auto rounded bg-red-50 p-4 text-left text-sm text-red-800">
          {error.stack}
        </pre>
      )}
    </div>
  )
}

// Inline section fallback (section boundary)
function SectionErrorFallback({ error, reset }: FallbackProps) {
  return (
    <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
      <p className="text-red-700 font-medium">Failed to load this section</p>
      <button onClick={reset} className="mt-3 text-sm text-red-600 underline hover:no-underline">
        Retry
      </button>
    </div>
  )
}

// Toast-style fallback (non-critical widget)
function WidgetErrorFallback({ name }: { name: string }) {
  return (
    <div className="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700">
      <span>⚠</span>
      <span>{name} unavailable</span>
    </div>
  )
}

Retry Mechanism in Error Boundaries

import { ErrorBoundary } from 'react-error-boundary'
import { useState, useCallback } from 'react'

function RetryFallback({ error, reset }: FallbackProps) {
  const [retries, setRetries] = useState(0)
  const maxRetries = 3

  const handleRetry = useCallback(() => {
    if (retries < maxRetries) {
      setRetries(r => r + 1)
      reset()
    }
  }, [retries, reset])

  return (
    <div className="rounded-lg border bg-white p-6 text-center">
      <p className="font-medium text-gray-900">Failed to load</p>
      {retries < maxRetries ? (
        <button onClick={handleRetry} className="mt-4 btn btn-sm">
          Retry ({maxRetries - retries} left)
        </button>
      ) : (
        <p className="mt-4 text-sm text-gray-400">
          Please refresh the page or <a href="/support" className="underline">contact support</a>.
        </p>
      )}
    </div>
  )
}

Error Reporting (Sentry Integration)

import * as Sentry from '@sentry/nextjs'

export function reportError(
  error: Error,
  context?: Record<string, unknown>,
  info?: React.ErrorInfo
) {
  if (process.env.NODE_ENV === 'development') {
    console.error('[Error]', error, context)
    return
  }

  Sentry.withScope(scope => {
    if (context) scope.setExtras(context)
    if (info?.componentStack) scope.setExtra('componentStack', info.componentStack)
    Sentry.captureException(error)
  })
}

Offline Detection and Fallback UI

'use client'
import { useState, useEffect } from 'react'

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof window !== 'undefined' ? navigator.onLine : true
  )
  useEffect(() => {
    const on  = () => setIsOnline(true)
    const off = () => setIsOnline(false)
    window.addEventListener('online',  on)
    window.addEventListener('offline', off)
    return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off) }
  }, [])
  return isOnline
}

export function OfflineBanner() {
  const isOnline = useOnlineStatus()
  if (isOnline) return null
  return (
    <div role="status" className="fixed top-0 inset-x-0 z-50 bg-amber-500 py-2 text-center text-sm font-medium text-white">
      You are offline. Changes will sync when connection is restored.
    </div>
  )
}

Partial Failure Handling

// Multiple independent widgets — one fails, others keep working
export function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <WidgetErrorBoundary name="Revenue Chart"><RevenueChart /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="User Stats"><UserStats /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="Activity Feed"><ActivityFeed /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="Notifications"><NotificationPanel /></WidgetErrorBoundary>
    </div>
  )
}

// Graceful degradation: show stale data when fetch fails
async function StatsWidget() {
  try {
    const stats = await fetchStats()
    return <StatsDisplay stats={stats} fresh />
  } catch {
    const cached = await getCachedStats()
    if (cached) return <StatsDisplay stats={cached} stale />
    throw new Error('Stats unavailable')   // let error boundary handle
  }
}

Error Recovery (Reset on Navigation)

import { usePathname } from 'next/navigation'
import { useEffect, useRef } from 'react'
import { useErrorBoundary } from 'react-error-boundary'

function NavigationErrorReset() {
  const pathname = usePathname()
  const { resetBoundary } = useErrorBoundary()
  const prevPath = useRef(pathname)

  useEffect(() => {
    if (prevPath.current !== pathname) {
      prevPath.current = pathname
      resetBoundary()
    }
  }, [pathname, resetBoundary])

  return null
}

// Place inside ErrorBoundary to auto-reset on navigation
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <NavigationErrorReset />
  {children}
</ErrorBoundary>

Development vs Production Error Display

function ErrorFallback({ error, reset }: FallbackProps) {
  const isDev = process.env.NODE_ENV === 'development'
  return (
    <div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-6">
      <h2 className="font-semibold text-red-900">
        {isDev ? `Error: ${error.name}` : 'Something went wrong'}
      </h2>
      {isDev ? (
        <details className="mt-3">
          <summary className="cursor-pointer text-sm text-red-700">Stack trace</summary>
          <pre className="mt-2 overflow-auto text-xs text-red-800 whitespace-pre-wrap">{error.stack}</pre>
        </details>
      ) : (
        <p className="mt-2 text-sm text-red-700">An unexpected error occurred. Please try again.</p>
      )}
      <button onClick={reset} className="mt-4 text-sm font-medium text-red-700 underline">Try again</button>
    </div>
  )
}

Testing Error Boundaries

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ErrorBoundary } from 'react-error-boundary'

function BrokenComponent({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) throw new Error('Test error')
  return <div>Working fine</div>
}

describe('ErrorBoundary', () => {
  beforeEach(() => vi.spyOn(console, 'error').mockImplementation(() => {}))
  afterEach(() => vi.mocked(console.error).mockRestore())

  it('shows fallback UI when child throws', () => {
    render(
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <BrokenComponent shouldThrow />
      </ErrorBoundary>
    )
    expect(screen.getByRole('alert')).toBeInTheDocument()
    expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
  })

  it('resets and shows content after retry', async () => {
    const user = userEvent.setup()
    let shouldThrow = true
    function Toggle() {
      if (shouldThrow) throw new Error('Test error')
      return <div>Recovered</div>
    }
    render(<ErrorBoundary FallbackComponent={ErrorFallback}><Toggle /></ErrorBoundary>)
    shouldThrow = false
    await user.click(screen.getByRole('button', { name: /try again/i }))
    expect(screen.getByText('Recovered')).toBeInTheDocument()
  })
})

Decision Matrix

| Scope | Fallback Type | When |

|-------|--------------|------|

| Widget | Inline placeholder | Non-critical UI section |

| Section | Collapsed + retry button | Independent feature area |

| Page | Full page with retry | Page-level data failure |

| App | Full page with refresh | Unrecoverable state |

| Network | Toast + offline banner | Connectivity issues |


Original Patterns (preserved for reference)

Basic Error Boundary (react-error-boundary)

import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert" className="p-4 bg-red-50 border border-red-200 rounded">
      <h2 className="text-lg font-semibold text-red-800">Something went wrong</h2>
      <pre className="mt-2 text-sm text-red-600">{error.message}</pre>
      <button
        onClick={resetErrorBoundary}
        className="mt-3 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  );
}

// Usage
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <RiskyComponent />
</ErrorBoundary>

Error Boundary Hierarchy

App Error Boundary (full page fallback)
├── Layout Error Boundary (layout-level recovery)
│   ├── Sidebar Error Boundary (sidebar collapses gracefully)
│   └── Main Content Error Boundary
│       ├── Widget A Error Boundary (individual widget fails)
│       ├── Widget B Error Boundary
│       └── Widget C Error Boundary
// App-level: full page fallback with navigation
<ErrorBoundary FallbackComponent={FullPageError}>
  <Layout>
    {/* Section-level: isolated failures */}
    <ErrorBoundary FallbackComponent={SectionError}>
      <Dashboard />
    </ErrorBoundary>
    <ErrorBoundary FallbackComponent={SectionError}>
      <Analytics />
    </ErrorBoundary>
  </Layout>
</ErrorBoundary>

Next.js App Router (error.tsx)

// app/dashboard/error.tsx
'use client';

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to error reporting service
    reportError(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2>Dashboard could not load</h2>
      <p className="text-gray-500">{error.message}</p>
      <button onClick={reset} className="mt-4 btn-primary">
        Retry
      </button>
    </div>
  );
}

Fallback UI Patterns

// 1. Inline fallback (widget level)
function InlineFallback({ error }: { error: Error }) {
  return (
    <div className="p-3 text-sm text-gray-500 bg-gray-50 rounded">
      Unable to load this section
    </div>
  );
}

// 2. Toast notification (non-critical)
function ToastFallback({ error, resetErrorBoundary }: FallbackProps) {
  useEffect(() => {
    toast.error(error.message, {
      action: { label: 'Retry', onClick: resetErrorBoundary },
    });
  }, [error]);
  return null; // renders nothing, shows toast instead
}

// 3. Full page (critical)
function FullPageFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-2xl font-bold">Something went wrong</h1>
      <p className="mt-2 text-gray-600">Please try refreshing the page</p>
      <div className="mt-4 space-x-3">
        <button onClick={resetErrorBoundary}>Retry</button>
        <button onClick={() => window.location.reload()}>Refresh</button>
      </div>
    </div>
  );
}

Retry with Error Boundary

import { ErrorBoundary } from 'react-error-boundary';
import { QueryErrorResetBoundary } from '@tanstack/react-query';

// Auto-reset on navigation
<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onReset={() => {
    // Clear any cached state that caused the error
    queryClient.invalidateQueries();
  }}
  resetKeys={[pathname]} // auto-reset when URL changes
>
  <Outlet />
</ErrorBoundary>

// With TanStack Query
<QueryErrorResetBoundary>
  {({ reset }) => (
    <ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
      <SuspenseComponent />
    </ErrorBoundary>
  )}
</QueryErrorResetBoundary>

Error Reporting

function logError(error: Error, info: { componentStack?: string }) {
  // Send to Sentry/monitoring
  if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
    Sentry.captureException(error, {
      extra: { componentStack: info.componentStack },
    });
  }
}

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={logError}
>
  <App />
</ErrorBoundary>

Offline Detection

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  );

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Offline banner
function OfflineBanner() {
  const isOnline = useOnlineStatus();
  if (isOnline) return null;

  return (
    <div className="fixed top-0 inset-x-0 bg-yellow-500 text-center py-2 z-50">
      You are offline. Some features may be unavailable.
    </div>
  );
}

Testing Error Boundaries

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

function ThrowError() {
  throw new Error('Test error');
}

test('renders fallback on error', () => {
  const spy = vi.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByRole('alert')).toBeInTheDocument();
  expect(screen.getByText(/test error/i)).toBeInTheDocument();

  spy.mockRestore();
});

test('resets on retry click', async () => {
  let shouldThrow = true;
  function MaybeThrow() {
    if (shouldThrow) throw new Error('Boom');
    return <div>Recovered</div>;
  }

  render(
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MaybeThrow />
    </ErrorBoundary>
  );

  shouldThrow = false;
  await userEvent.click(screen.getByText('Try again'));
  expect(screen.getByText('Recovered')).toBeInTheDocument();
});

Decision Matrix

| Scope | Fallback Type | When |

|-------|--------------|------|

| Widget | Inline placeholder | Non-critical UI section |

| Section | Collapsed + retry button | Independent feature area |

| Page | Full page with retry | Page-level data failure |

| App | Full page with refresh | Unrecoverable state |

| Network | Toast + offline banner | Connectivity issues |

How to use it

Copy the folder

Take vibeeval/error-boundary from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm. Without those the skill loads but fails at the first command.