Error Handling
10 Nov 2026
8 min read

Error Boundaries and Graceful Degradation: Keeping Your App Alive

Implementing React Error Boundaries with user-friendly fallbacks, retry mechanisms, and error reporting.

Error Boundaries and Graceful Degradation

SpaceZero includes a production-ready Error Boundary component.

The Error Boundary

tsx

// components/infra/error-boundary.tsx

export class ErrorBoundary extends React.Component<

{ children: React.ReactNode },

{ hasError: boolean; error: Error | null }

> {

constructor(props: { children: React.ReactNode }) {

super(props);

this.state = { hasError: false, error: null };

}

 

static getDerivedStateFromError(error: Error) {

return { hasError: true, error };

}

 

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {

console.error('[SpaceZero ErrorBoundary]:', error, errorInfo);

}

 

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

handleReload = () => window.location.reload();

 

render() {

if (this.state.hasError) {

return (

<Modal>

<ModalBackdrop />

<ModalDialog>

<ModalHeader>

<ModalClose onClick={this.handleReset}>

<Icon name="X" />

</ModalClose>

<ModalIcon>

<Icon name="AlertTriangle" />

</ModalIcon>

<ModalTitle>Something went wrong</ModalTitle>

</ModalHeader>

<ModalDescription>

An unexpected error occurred. You can try to reload the page.

</ModalDescription>

<ModalBody>

{this.state.error && (

<Notice variant="destructive">{this.state.error.message}</Notice>

)}

</ModalBody>

<ModalFooter className="flex gap-3">

<Button variant="default" onClick={this.handleReset}>

Try Again

</Button>

<Button variant="action" onClick={this.handleReload}>

Reload

</Button>

</ModalFooter>

</ModalDialog>

</Modal>

);

}

return this.props.children;

}

}

The fallback is a modal, so it works at any nesting depth without needing page-level layout.

Usage

Wrap your app root:

tsx

// App.tsx

<ErrorBoundary>

<ToastProvider>{page}</ToastProvider>

</ErrorBoundary>

Features

1. Try Again Button

Resets error state, re-renders children:

tsx

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

Useful for transient errors (network blip, race condition).

2. Reload Button

Full page reload:

tsx

handleReload = () => window.location.reload();

For persistent errors requiring fresh state.

3. Error Display

Shows the error message in a destructive Notice:

tsx

<Notice variant="destructive">{this.state.error.message}</Notice>

4. Error Logging

tsx

componentDidCatch(error, errorInfo) {

console.error('[SpaceZero ErrorBoundary]:', error, errorInfo);

// Send to Sentry, LogRocket, etc.

}

Best Practices

1. Granular Boundaries

Wrap different sections independently:

tsx

<ErrorBoundary>

<Header />

</ErrorBoundary>

<ErrorBoundary>

<PageMain>

<PageContent />

</PageMain>

</ErrorBoundary>

<ErrorBoundary>

<Footer />

</ErrorBoundary>

2. Fallback UI Matching Design

Use same components (Card, Button) for consistency.

3. Don't Swallow Errors

Always log to console/error tracking.

4. Provide Recovery Actions

At minimum: "Try Again" and "Reload".

Integration with Toast

tsx

// For non-fatal errors, use Toast instead

const { toast } = useToast();

toast.destructive('Failed to save', 'Please try again');

Testing

  1. Throw error in component: throw new Error('Test')
  2. Verify fallback UI appears
  3. Click "Try Again" → re-renders
  4. Click "Reload" → full reload
  5. Check console for error log

Benefits

  • User Retention: App doesn't crash completely
  • Debugging: Error info logged automatically
  • Recovery: Users can self-recover
  • Professional: Graceful degradation vs white screen