Error Handling

Error handling patterns and best practices in DesQTA

Error Handling Overview

DesQTA implements comprehensive error handling at multiple levels to ensure a robust user experience and proper error recovery.

Error Handling Strategy

Error Layers

User Interface
    │
    ▼
Component Error Boundary
    │
    ▼
Service Error Handling
    │
    ▼
Tauri Command Error Handling
    │
    ▼
Rust Error Handling

Frontend Error Handling

Component-Level Errors

Use error boundaries to catch component errors:

<script lang="ts">
  import ErrorBoundary from '$lib/components/ErrorBoundary.svelte';
  
  let error = $state<Error | null>(null);
</script>

<ErrorBoundary {error}>
  {#snippet children()}
    <MyComponent />
  {/snippet}
  
  {#snippet fallback(error)}
    <div class="p-4 border border-red-500 rounded-lg">
      <h3 class="text-red-600 font-semibold">Error</h3>
      <p class="text-sm text-red-500">{error.message}</p>
      <button onclick={() => error = null}>Retry</button>
    </div>
  {/snippet}
</ErrorBoundary>

Service-Level Errors

Handle errors in services:

// src/lib/services/dataService.ts
import { logger } from '$lib/utils/logger';
import { errorService } from '$lib/services/errorService';

export const dataService = {
  async loadData(): Promise<Data> {
    try {
      logger.logFunctionEntry('dataService', 'loadData');
      const data = await invoke('get_data');
      logger.logFunctionExit('dataService', 'loadData', data);
      return data;
    } catch (error) {
      logger.error('dataService', 'loadData', 'Failed to load data', { error });
      errorService.handleError(error);
      throw error;
    }
  }
};

Error Service

Centralized error handling:

// src/lib/services/errorService.ts
import { toast } from '$lib/stores/toast';
import { logger } from '$lib/utils/logger';

export const errorService = {
  handleError(error: unknown, context?: string) {
    const message = this.getErrorMessage(error);
    const errorObj = error instanceof Error ? error : new Error(String(error));
    
    // Log error
    logger.error('errorService', 'handleError', message, { error: errorObj, context });
    
    // Show user-friendly message
    toast.error(this.getUserMessage(error));
    
    // Report to error tracking (if configured)
    this.reportError(errorObj, context);
  },
  
  getErrorMessage(error: unknown): string {
    if (error instanceof Error) return error.message;
    if (typeof error === 'string') return error;
    return 'An unknown error occurred';
  },
  
  getUserMessage(error: unknown): string {
    const message = this.getErrorMessage(error);
    
    // Map technical errors to user-friendly messages
    if (message.includes('network')) {
      return 'Network error. Please check your connection.';
    }
    if (message.includes('session')) {
      return 'Session expired. Please log in again.';
    }
    
    return 'Something went wrong. Please try again.';
  },
  
  reportError(error: Error, context?: string) {
    // Send to error tracking service
    // Example: Sentry, LogRocket, etc.
  }
};

Error Types

Network Errors

class NetworkError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public response?: any
  ) {
    super(message);
    this.name = 'NetworkError';
  }
}

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new NetworkError('Request failed', response.status, response);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof NetworkError) {
      // Handle network error
    }
    throw error;
  }
}

Validation Errors

class ValidationError extends Error {
  constructor(
    message: string,
    public field?: string,
    public errors?: Record<string, string[]>
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

function validateData(data: any) {
  const errors: Record<string, string[]> = {};
  
  if (!data.name) {
    errors.name = ['Name is required'];
  }
  
  if (errors.name || errors.email) {
    throw new ValidationError('Validation failed', undefined, errors);
  }
}

Business Logic Errors

class BusinessError extends Error {
  constructor(
    message: string,
    public code: string,
    public context?: Record<string, any>
  ) {
    super(message);
    this.name = 'BusinessError';
  }
}

async function processAssessment(assessment: Assessment) {
  if (assessment.dueDate < new Date()) {
    throw new BusinessError(
      'Cannot modify overdue assessment',
      'ASSESSMENT_OVERDUE',
      { assessmentId: assessment.id }
    );
  }
}

Error Display Patterns

Toast Notifications

import { toast } from '$lib/stores/toast';

try {
  await saveData();
  toast.success('Saved successfully');
} catch (error) {
  toast.error('Failed to save');
}

Error Messages in Forms

<script lang="ts">
  let email = $state('');
  let emailError = $state<string | null>(null);
  
  function validateEmail() {
    if (!email.includes('@')) {
      emailError = 'Invalid email address';
      return false;
    }
    emailError = null;
    return true;
  }
</script>

<Input
  bind:value={email}
  error={emailError}
  onblur={validateEmail}
/>

Error States in Components

<script lang="ts">
  import AsyncWrapper from '$lib/components/ui/AsyncWrapper';
  
  let data = $state<any[]>([]);
  let loading = $state(true);
  let error = $state<Error | null>(null);
  
  async function loadData() {
    loading = true;
    error = null;
    try {
      data = await fetchData();
    } catch (e) {
      error = e instanceof Error ? e : new Error('Failed to load');
    } finally {
      loading = false;
    }
  }
</script>

<AsyncWrapper {loading} {error} data={data}>
  {#snippet children(data)}
    {#each data as item}
      <ItemComponent {item} />
    {/each}
  {/snippet}
</AsyncWrapper>

Error Recovery

Retry Logic

async function retry<T>(
  fn: () => Promise<T>,
  maxAttempts: number = 3,
  delay: number = 1000
): Promise<T> {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxAttempts - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
    }
  }
  throw new Error('Max attempts reached');
}

// Usage
const data = await retry(() => fetchData(), 3, 1000);

Fallback Values

async function loadDataWithFallback() {
  try {
    return await fetchData();
  } catch (error) {
    logger.warn('dataService', 'loadData', 'Using fallback data', { error });
    return getCachedData() || getDefaultData();
  }
}

Graceful Degradation

<script lang="ts">
  let data = $state<any[]>([]);
  let error = $state<Error | null>(null);
  
  async function loadData() {
    try {
      data = await fetchData();
    } catch (e) {
      error = e instanceof Error ? e : new Error('Failed to load');
      // Show cached data if available
      const cached = getCachedData();
      if (cached) {
        data = cached;
      }
    }
  }
</script>

{#if error && data.length === 0}
  <ErrorMessage {error} onRetry={loadData} />
{:else}
  <DataComponent {data} />
  {#if error}
    <div class="text-yellow-600">
      Showing cached data. Some features may be limited.
    </div>
  {/if}
{/if}

Backend Error Handling

Tauri Command Errors

#[tauri::command]
pub async fn risky_operation() -> Result<String, String> {
    match do_something() {
        Ok(result) => Ok(result),
        Err(e) => {
            // Log error
            eprintln!("Error: {}", e);
            // Return user-friendly error
            Err(format!("Operation failed: {}", e))
        }
    }
}

Error Types in Rust

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Network error: {0}")]
    Network(String),
    
    #[error("Validation error: {0}")]
    Validation(String),
    
    #[error("Not found: {0}")]
    NotFound(String),
}

#[tauri::command]
pub async fn get_data(id: i32) -> Result<Data, String> {
    let data = fetch_data(id).await
        .map_err(|e| AppError::Network(e.to_string()))?;
    
    Ok(data)
}

Error Logging

Structured Logging

import { logger } from '$lib/utils/logger';

try {
  await operation();
} catch (error) {
  logger.error(
    'context',
    'function',
    'Operation failed',
    {
      error,
      userId: user.id,
      timestamp: new Date().toISOString(),
      context: { /* additional context */ }
    }
  );
}

Error Tracking

// Report to error tracking service
function reportError(error: Error, context?: Record<string, any>) {
  // Example: Sentry
  Sentry.captureException(error, {
    extra: context
  });
  
  // Example: Custom tracking
  fetch('/api/errors', {
    method: 'POST',
    body: JSON.stringify({
      message: error.message,
      stack: error.stack,
      context
    })
  });
}

Best Practices

1. Handle Errors at the Right Level

// ✅ Good - Handle in service
async function loadData() {
  try {
    return await fetchData();
  } catch (error) {
    // Handle and transform
    throw new UserFriendlyError('Failed to load data');
  }
}

// ❌ Avoid - Let errors bubble up unhandled
async function loadData() {
  return await fetchData(); // Error not handled
}

2. Provide User-Friendly Messages

// ✅ Good - User-friendly
throw new Error('Unable to connect. Please check your internet connection.');

// ❌ Avoid - Technical
throw new Error('ECONNREFUSED 127.0.0.1:3000');

3. Log Errors with Context

// ✅ Good - With context
logger.error('service', 'function', 'Error message', {
  error,
  userId: user.id,
  action: 'save_assessment'
});

// ❌ Avoid - No context
console.error(error);

4. Don't Swallow Errors

// ✅ Good - Re-throw or handle
try {
  await operation();
} catch (error) {
  logger.error('context', 'function', 'Failed', { error });
  throw error; // Re-throw
}

// ❌ Avoid - Swallow error
try {
  await operation();
} catch (error) {
  // Error ignored
}

5. Use Error Boundaries

<!-- ✅ Good - Error boundary -->
<ErrorBoundary>
  <ComponentThatMightError />
</ErrorBoundary>

<!-- ❌ Avoid - No error handling -->
<ComponentThatMightError />

Error Handling Checklist

  • Errors handled at appropriate levels
  • User-friendly error messages
  • Error logging implemented
  • Error boundaries in place
  • Retry logic for transient errors
  • Fallback values provided
  • Error tracking configured
  • Error recovery mechanisms

Next Steps