Initial Release help-frontend

Dr. Frontend

App Level Error Modal

A pattern for implementing application-wide error handling using React Context. Errors are reported from anywhere in the app and displayed in a centralized modal.

What This Pattern Does

This example demonstrates a production-ready pattern for handling errors at the application level. Instead of handling errors locally in each component, errors are reported to a central context and displayed in a consistent modal dialog.

ErrorContext + ErrorProvider

Manages error state, provides reportError() function, and maintains error history for debugging.

ModalContext + ModalProvider

Renders the error modal dialog, auto-shows on new errors, and provides retry/dismiss actions.

Severity Levels

Supports error,warning, andinfo with appropriate styling.

Retry Actions

Errors can include a retry callback that users can trigger directly from the modal.

Live Demo

Trigger Error Modal

Click the button below to simulate an application error and see the modal in action.

Error Severity Levels

The modal adapts its appearance based on the error severity.

Realistic API Error

Simulates a detailed API error with stack trace and retry functionality.

Error History

All errors reported during this session.

No errors recorded yet.

Implementation Guide

Step 1: Add Providers to App Root

// App.tsx or main.tsx
import { ErrorProvider } from './contexts/ErrorContext';
import { ModalProvider } from './contexts/ModalContext';

function App() {
  return (
    <ErrorProvider>
      <ModalProvider autoShowOnError>
        <Router>
          <YourRoutes />
        </Router>
      </ModalProvider>
    </ErrorProvider>
  );
}

Step 2: Report Errors from Components

import { useError } from './contexts/ErrorContext';

function MyComponent() {
  const { reportError } = useError();

  const handleSubmit = async () => {
    try {
      await api.saveData(data);
    } catch (err) {
      reportError({
        title: 'Save Failed',
        message: err.message,
        severity: 'error',
        code: err.code,
        retryAction: () => handleSubmit(),
      });
    }
  };
}

Step 3: Handle API Errors Globally

// api-client.ts
import { useError } from './contexts/ErrorContext';

// Create an error reporter for non-component code
let errorReporter: ReturnType<typeof useError>['reportError'] | null = null;

export function setErrorReporter(reporter: typeof errorReporter) {
  errorReporter = reporter;
}

// Use in API interceptor
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (errorReporter) {
      errorReporter({
        title: 'API Error',
        message: error.message,
        severity: 'error',
        code: error.response?.status?.toString(),
      });
    }
    return Promise.reject(error);
  }
);

Key Files

  • ErrorContext.tsx - Error state management
  • ModalContext.tsx - Modal rendering
  • AppErrorModalDemo.tsx - Demo component

Testing Components with Context

Components that use these contexts need to be wrapped in providers during testing. Create a reusable test wrapper to avoid repetition.

Test Wrapper Utility

// test-utils.tsx
import { render, type RenderOptions } from '@testing-library/react';
import { ErrorProvider } from './contexts/ErrorContext';
import { ModalProvider } from './contexts/ModalContext';

interface WrapperProps {
  children: React.ReactNode;
}

function AllProviders({ children }: WrapperProps) {
  return (
    <ErrorProvider>
      <ModalProvider autoShowOnError={false}>
        {children}
      </ModalProvider>
    </ErrorProvider>
  );
}

function customRender(
  ui: React.ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, { wrapper: AllProviders, ...options });
}

// Re-export everything from testing-library
export * from '@testing-library/react';
export { customRender as render };

Example Test

// MyComponent.test.tsx
import { render, screen, fireEvent } from './test-utils';
import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
  it('shows error modal when API fails', async () => {
    // Mock API to fail
    vi.spyOn(api, 'saveData').mockRejectedValue(
      new Error('Network error')
    );

    render(<MyComponent />);
    
    fireEvent.click(screen.getByText('Save'));
    
    // Error modal should appear
    expect(await screen.findByText('Save Failed')).toBeInTheDocument();
    expect(screen.getByText('Network error')).toBeInTheDocument();
  });

  it('retries action when retry button clicked', async () => {
    const saveSpy = vi.spyOn(api, 'saveData')
      .mockRejectedValueOnce(new Error('Fail'))
      .mockResolvedValueOnce({ success: true });

    render(<MyComponent />);
    
    fireEvent.click(screen.getByText('Save'));
    await screen.findByText('Save Failed');
    
    fireEvent.click(screen.getByText('Retry'));
    
    expect(saveSpy).toHaveBeenCalledTimes(2);
  });
});

AppError Interface

interface AppError {
  id: string;           // Auto-generated UUID
  title: string;        // Short error title
  message: string;      // User-friendly message
  severity: 'error' | 'warning' | 'info';
  timestamp: Date;      // Auto-set when reported
  details?: string;     // Technical details (stack trace, etc.)
  code?: string;        // Error code for support
  retryAction?: () => void;  // Optional retry callback
}