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 managementModalContext.tsx- Modal renderingAppErrorModalDemo.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
}