11 KiB
Error Display Strategy
Date: 2025-01-27
Action: 3.1.1.5 - Create error display strategy document
Status: ✅ Complete
Overview
This document defines the strategy for when to use toast.error() vs ErrorDisplay component for error presentation across the application. This strategy ensures consistent, user-friendly error handling while maintaining appropriate UX patterns for different error types.
Core Principles
- Persistent errors → Use
ErrorDisplay(user needs to see and potentially act on the error) - Transient errors → Use
toast.error()(quick feedback for temporary actions) - Form validation → Keep inline validation errors (field-level feedback)
- Critical errors → Use
ErrorDisplaywith retry capability - User action errors → Use
ErrorDisplaybanner (dismissible, contextual)
Decision Tree
Is the error related to form validation?
├─ YES → Use inline validation error (text-red-500, keep existing pattern)
└─ NO → Continue
Is the error from a query (data fetching)?
├─ YES → Use ErrorDisplay card/inline variant with retry
└─ NO → Continue
Is the error from a mutation (data update)?
├─ YES → Use ErrorDisplay banner variant (dismissible)
└─ NO → Continue
Is the error from a quick action (copy link, quick toggle)?
├─ YES → Use toast.error() (transient feedback)
└─ NO → Continue
Is the error critical (network failure, auth failure, runtime crash)?
├─ YES → Use ErrorDisplay with appropriate variant and retry
└─ NO → Use toast.error() as fallback
ErrorDisplay Usage Guidelines
When to Use ErrorDisplay
1. Query Errors (Data Fetching)
Use: ErrorDisplay card or inline variant
Variant: card for page-level errors, inline for component-level errors
Features: Retry button, error details (dev mode)
Examples:
- Failed to load tracks/library
- Failed to load playlist details
- Failed to load user profile
- Failed to load marketplace items
{
isError && (
<ErrorDisplay
error={error}
variant="card"
severity="error"
context={{ action: 'fetching tracks', resource: 'tracks' }}
onRetry={() => refetch()}
/>
);
}
2. Mutation Errors (Data Updates)
Use: ErrorDisplay banner variant
Variant: banner (dismissible, appears at top of page/content)
Features: Dismiss button, contextual error message
Examples:
- Failed to add track to playlist
- Failed to update profile
- Failed to delete resource
- Failed to share resource
{
mutationError && (
<ErrorDisplay
error={mutationError}
variant="banner"
severity="error"
onDismiss={() => setMutationError(null)}
/>
);
}
3. Network Errors (API Failures)
Use: ErrorDisplay banner variant
Variant: banner or card depending on context
Features: Retry button, network-specific messaging
Examples:
- Connection timeout
- Server unavailable
- Network request failed
<ErrorDisplay
error={networkError}
variant="banner"
severity="error"
context={{ action: 'connecting to server', resource: 'api' }}
onRetry={() => retryRequest()}
/>
4. Runtime Errors (Component Crashes)
Use: ErrorDisplay in ErrorBoundary fallback
Variant: card or modal
Features: Error details, reload page action
Examples:
- Component render error
- Unhandled exception
- React error boundary catch
<ErrorBoundary
fallback={
<ErrorDisplay
error={error}
variant="card"
severity="error"
actions={[
{ label: 'Reload Page', onClick: () => window.location.reload() },
]}
/>
}
>
{children}
</ErrorBoundary>
5. Auth Errors (Authentication Failures)
Use: ErrorDisplay inline variant
Variant: inline
Features: Contextual auth error message
Examples:
- Login failed
- Session expired
- Unauthorized access
<ErrorDisplay
error={authError}
variant="inline"
severity="error"
context={{ action: 'authenticating', resource: 'auth' }}
/>
6. Player Errors (Audio Playback)
Use: ErrorDisplay card variant
Variant: card
Features: Retry button, error type detection
Examples:
- Audio decode error
- Network error during playback
- Source file not found
<ErrorDisplay
error={playerError}
variant="card"
severity="error"
context={{ action: 'playing audio', resource: 'player' }}
onRetry={() => retryPlayback()}
/>
When NOT to Use ErrorDisplay
1. Form Validation Errors
Use: Inline validation errors (existing pattern)
Reason: Field-level feedback is more appropriate
Pattern: Keep existing text-red-500 inline errors
// ✅ Keep this pattern
{
errors.field && (
<p className="text-sm text-red-500">{errors.field.message}</p>
);
}
2. Quick Action Errors (Transient)
Use: toast.error()
Reason: Quick feedback for temporary actions
Examples:
- Copy link to clipboard
- Quick toggle actions
- Temporary state changes
// ✅ Keep toast for quick actions
try {
await navigator.clipboard.writeText(link);
toast.success('Link copied');
} catch {
toast.error('Failed to copy link');
}
3. Validation Messages (Non-Critical)
Use: toast.error() or inline validation
Reason: User input validation feedback
Examples:
- "Username is required"
- "Please type DELETE to confirm"
- "Cart is empty"
// ✅ Keep toast for validation messages
if (!username) {
toast.error('Username is required');
return;
}
Variant Selection Guide
inline Variant
Use when:
- Error is contextual to a specific component
- Error appears within content flow
- Error doesn't need to interrupt user flow
Examples:
- Form submission errors
- Component-level query errors
- Inline validation errors (if not using form validation pattern)
banner Variant
Use when:
- Error affects page-level functionality
- Error is dismissible
- Error should be prominent but not blocking
Examples:
- Mutation errors (add/update/delete)
- Network errors
- Session expiration warnings
card Variant
Use when:
- Error replaces main content
- Error needs visual prominence
- Error is part of content layout
Examples:
- Query errors (no data to show)
- Player errors (replaces player UI)
- Profile load errors
modal Variant
Use when:
- Error is critical and blocking
- User must acknowledge error
- Error requires immediate attention
Examples:
- Critical system errors
- Payment failures
- Security violations
Severity Selection Guide
error Severity (Default)
Use when:
- Operation failed
- Data cannot be loaded
- Action cannot be completed
Visual: Red colors, AlertCircle icon
warning Severity
Use when:
- Operation partially succeeded
- Warning that doesn't block functionality
- User should be aware but can continue
Visual: Yellow/gold colors, AlertTriangle icon
info Severity
Use when:
- Informational message
- Non-critical notification
- Helpful context
Visual: Cyan colors, Info icon
Migration Strategy
Phase 1: Query Errors (HIGH Priority)
- Replace query error displays with ErrorDisplay card variant
- Add retry functionality
- Files: LibraryPage ✅, TrackDetailPage, MarketplaceHome, RolesPage, SettingsPage
Phase 2: Mutation Errors (MEDIUM Priority)
- Replace mutation toast.errors with ErrorDisplay banner variant
- Add dismiss functionality
- Files: All feature files with mutations
Phase 3: Component Errors (MEDIUM Priority)
- Replace PlayerError with ErrorDisplay card variant
- Replace AuthErrorMessage with ErrorDisplay inline variant
- Update ErrorBoundary fallback UI
Phase 4: Network Errors (HIGH Priority)
- Replace API client toast.errors with ErrorDisplay banner
- Add retry functionality
- File: api/client.ts
Phase 5: Keep Toast for Transient Actions (LOW Priority)
- Keep toast.error() for copy link, quick actions
- Document exceptions
- No changes needed
Code Examples
Query Error Pattern
const { data, isError, error, refetch } = useQuery({
queryKey: ['tracks'],
queryFn: fetchTracks,
});
return (
<>
{isError ? (
<ErrorDisplay
error={error}
variant="card"
severity="error"
context={{ action: 'fetching tracks', resource: 'tracks' }}
onRetry={() => refetch()}
/>
) : (
<TrackList tracks={data} />
)}
</>
);
Mutation Error Pattern
const [mutationError, setMutationError] = useState<Error | null>(null);
const handleMutation = async () => {
try {
setMutationError(null);
await mutateAsync(data);
toast.success('Success');
} catch (error) {
const apiError = parseApiError(error);
setMutationError(new Error(apiError.message));
}
};
return (
<>
{mutationError && (
<ErrorDisplay
error={mutationError}
variant="banner"
severity="error"
onDismiss={() => setMutationError(null)}
/>
)}
<Form onSubmit={handleMutation} />
</>
);
Form Validation Pattern (Keep)
// ✅ Keep existing pattern for form validation
<FormField label="Email" error={errors.email?.message}>
<Input type="email" {...register('email')} />
</FormField>
Quick Action Pattern (Keep Toast)
// ✅ Keep toast for quick actions
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(link);
toast.success('Link copied');
} catch {
toast.error('Failed to copy link');
}
};
Accessibility Considerations
ErrorDisplay Component
- ✅ ARIA
role="alert"andaria-live="polite" - ✅ Keyboard navigation for retry/dismiss buttons
- ✅ Screen reader announcements
- ✅ Focus management
Toast Notifications
- ✅ ARIA live regions (handled by toast library)
- ✅ Auto-dismiss with appropriate timing
- ✅ Keyboard accessible
Form Validation
- ✅ Inline errors associated with form fields
- ✅ ARIA
aria-invalidandaria-describedby - ✅ Screen reader announcements
Testing Strategy
- Query Errors: Test retry functionality
- Mutation Errors: Test dismiss functionality
- Network Errors: Test retry and offline handling
- Form Validation: Test inline error display
- Toast Errors: Test transient error display
Rollback Plan
If ErrorDisplay causes issues:
- Revert to toast.error() for affected areas
- Keep ErrorDisplay for query errors (most critical)
- Gradually reintroduce ErrorDisplay for mutations