veza/apps/web/src/components/ui/ERROR_DISPLAY_STRATEGY.md

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

  1. Persistent errors → Use ErrorDisplay (user needs to see and potentially act on the error)
  2. Transient errors → Use toast.error() (quick feedback for temporary actions)
  3. Form validation → Keep inline validation errors (field-level feedback)
  4. Critical errors → Use ErrorDisplay with retry capability
  5. User action errors → Use ErrorDisplay banner (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)

  1. Replace query error displays with ErrorDisplay card variant
  2. Add retry functionality
  3. Files: LibraryPage , TrackDetailPage, MarketplaceHome, RolesPage, SettingsPage

Phase 2: Mutation Errors (MEDIUM Priority)

  1. Replace mutation toast.errors with ErrorDisplay banner variant
  2. Add dismiss functionality
  3. Files: All feature files with mutations

Phase 3: Component Errors (MEDIUM Priority)

  1. Replace PlayerError with ErrorDisplay card variant
  2. Replace AuthErrorMessage with ErrorDisplay inline variant
  3. Update ErrorBoundary fallback UI

Phase 4: Network Errors (HIGH Priority)

  1. Replace API client toast.errors with ErrorDisplay banner
  2. Add retry functionality
  3. File: api/client.ts

Phase 5: Keep Toast for Transient Actions (LOW Priority)

  1. Keep toast.error() for copy link, quick actions
  2. Document exceptions
  3. 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" and aria-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-invalid and aria-describedby
  • Screen reader announcements

Testing Strategy

  1. Query Errors: Test retry functionality
  2. Mutation Errors: Test dismiss functionality
  3. Network Errors: Test retry and offline handling
  4. Form Validation: Test inline error display
  5. Toast Errors: Test transient error display

Rollback Plan

If ErrorDisplay causes issues:

  1. Revert to toast.error() for affected areas
  2. Keep ErrorDisplay for query errors (most critical)
  3. Gradually reintroduce ErrorDisplay for mutations

References