home / skills / yosrbennagra / 3sc / error-handling

error-handling skill

/.github/skills/error-handling

npx playbooks add skill yosrbennagra/3sc --skill error-handling

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
10.5 KB
---
name: error-handling
description: Global error handling for the 3SC widget host. Covers exception handling, crash reporting, user-friendly error surfaces, and recovery strategies.
---

# Error Handling

## Overview

Provide consistent, user-safe error handling across the shell, widgets, and background services. Users should never see stack traces or cryptic error messages.

## Definition of Done (DoD)

- [ ] Global handlers registered in App.xaml.cs for all exception sources
- [ ] All exceptions logged with correlation IDs and context
- [ ] User sees friendly, actionable error messages
- [ ] Crash reports saved locally with sanitized data
- [ ] No swallowed exceptions without explicit justification
- [ ] Widget errors don't crash the host application

## Global Exception Handlers

### App.xaml.cs Registration

```csharp
private void RegisterGlobalExceptionHandlers()
{
    // UI thread exceptions
    DispatcherUnhandledException += OnDispatcherUnhandledException;
    
    // Background thread exceptions
    AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
    
    // Task exceptions that aren't observed
    TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
}

private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
    var correlationId = CorrelationContext.Current ?? Guid.NewGuid().ToString("N")[..8];
    
    Log.Error(e.Exception, 
        "Unhandled UI exception. CorrelationId: {CorrelationId}", correlationId);
    
    // Save crash report
    CrashReportService.SaveReport(e.Exception, correlationId);
    
    // Show user-friendly message
    ShowErrorToUser(e.Exception, correlationId);
    
    // Prevent app crash for recoverable errors
    e.Handled = IsRecoverableError(e.Exception);
}

private void OnAppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    var exception = e.ExceptionObject as Exception;
    var correlationId = Guid.NewGuid().ToString("N")[..8];
    
    Log.Fatal(exception, 
        "Fatal unhandled exception. Terminating: {IsTerminating}, CorrelationId: {CorrelationId}", 
        e.IsTerminating, correlationId);
    
    CrashReportService.SaveReport(exception, correlationId);
}

private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
    Log.Error(e.Exception, "Unobserved task exception");
    
    // Prevent app crash - log and continue
    e.SetObserved();
}
```

### Recoverable vs Fatal Errors

```csharp
private static bool IsRecoverableError(Exception ex) => ex switch
{
    // Network issues - recoverable
    HttpRequestException => true,
    TaskCanceledException => true,
    
    // Database issues - might recover
    DbUpdateException => true,
    
    // Widget errors - definitely recoverable (isolate the widget)
    WidgetLoadException => true,
    WidgetExecutionException => true,
    
    // Memory/system issues - not recoverable
    OutOfMemoryException => false,
    StackOverflowException => false,
    AccessViolationException => false,
    
    _ => true  // Default to recoverable
};
```

## ViewModel Error Handling

### Async Command Pattern

```csharp
[RelayCommand]
private async Task LoadWidgetsAsync(CancellationToken ct)
{
    IsLoading = true;
    ErrorMessage = null;
    
    try
    {
        var widgets = await _repository.GetAllAsync(ct);
        Widgets = new ObservableCollection<WidgetViewModel>(
            widgets.Select(w => new WidgetViewModel(w)));
    }
    catch (OperationCanceledException)
    {
        // User cancelled - not an error
        Log.Debug("Widget loading cancelled by user");
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Failed to load widgets");
        ErrorMessage = "Failed to load widgets. Please try again.";
        
        // Optionally show toast/notification
        _notifications.ShowError("Could not load widgets");
    }
    finally
    {
        IsLoading = false;
    }
}
```

### Error Display Patterns

```csharp
public partial class WidgetLibraryViewModel : ObservableObject
{
    [ObservableProperty]
    private string? _errorMessage;
    
    [ObservableProperty]
    private ErrorSeverity _errorSeverity = ErrorSeverity.None;
    
    public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
    
    private void SetError(string message, ErrorSeverity severity = ErrorSeverity.Error)
    {
        ErrorMessage = message;
        ErrorSeverity = severity;
        OnPropertyChanged(nameof(HasError));
    }
    
    private void ClearError()
    {
        ErrorMessage = null;
        ErrorSeverity = ErrorSeverity.None;
        OnPropertyChanged(nameof(HasError));
    }
}

public enum ErrorSeverity { None, Info, Warning, Error }
```

## User Error Messages

### Safe Message Mapping

```csharp
public static class UserMessages
{
    public static string FromException(Exception ex) => ex switch
    {
        HttpRequestException => 
            "Unable to connect to the server. Please check your internet connection.",
        
        DbUpdateException => 
            "Failed to save changes. Please try again.",
        
        FileNotFoundException => 
            "The requested file could not be found.",
        
        UnauthorizedAccessException => 
            "Access denied. You may need to run as administrator.",
        
        WidgetLoadException wle => 
            $"Widget '{wle.WidgetKey}' failed to load. It may be corrupted or incompatible.",
        
        TimeoutException => 
            "The operation timed out. Please try again.",
        
        _ => "An unexpected error occurred. Please try again."
    };
    
    public static string WithCorrelationId(string message, string correlationId) =>
        $"{message}\n\nReference: {correlationId}";
}
```

## Widget Error Isolation

```csharp
public async Task<bool> SafeLoadWidgetAsync(string widgetKey)
{
    try
    {
        var widget = await _loader.LoadWidgetAsync(widgetKey);
        await widget.InitializeAsync();
        return true;
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Widget {WidgetKey} failed to load", widgetKey);
        
        // Mark widget as problematic
        await _widgetRepo.MarkAsFailedAsync(widgetKey, ex.Message);
        
        // Notify user but don't crash
        _notifications.ShowError($"Widget '{widgetKey}' failed to load");
        
        return false;
    }
}

// Widget execution wrapper
public void SafeExecuteWidgetAction(string widgetKey, Action action)
{
    try
    {
        action();
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Widget {WidgetKey} action failed", widgetKey);
        
        // Optionally disable the widget
        if (ShouldDisableWidget(ex))
        {
            DisableWidget(widgetKey, "Repeated failures");
        }
    }
}
```

## Crash Reports

### Crash Report Service

```csharp
public static class CrashReportService
{
    private static readonly string CrashFolder = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "3SC", "crashes");
    
    public static void SaveReport(Exception? exception, string correlationId)
    {
        try
        {
            Directory.CreateDirectory(CrashFolder);
            
            var report = new CrashReport
            {
                Timestamp = DateTimeOffset.UtcNow,
                CorrelationId = correlationId,
                AppVersion = GetAppVersion(),
                OsVersion = Environment.OSVersion.ToString(),
                ExceptionType = exception?.GetType().FullName,
                Message = exception?.Message,
                StackTrace = SanitizeStackTrace(exception?.ToString()),
                AdditionalContext = GatherContext()
            };
            
            var fileName = $"crash_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{correlationId}.json";
            var filePath = Path.Combine(CrashFolder, fileName);
            
            File.WriteAllText(filePath, JsonSerializer.Serialize(report, new JsonSerializerOptions
            {
                WriteIndented = true
            }));
            
            // Cleanup old reports (keep last 50)
            CleanupOldReports(maxReports: 50);
        }
        catch
        {
            // Never throw from crash reporting
        }
    }
    
    private static string? SanitizeStackTrace(string? stackTrace)
    {
        if (string.IsNullOrEmpty(stackTrace)) return null;
        
        var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        return stackTrace.Replace(userProfile, "[USER]");
    }
}
```

## Error UI Components

### Error Banner (XAML)

```xml
<Border x:Name="ErrorBanner"
        Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}"
        Background="{DynamicResource ErrorBackgroundBrush}"
        Padding="12,8">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        
        <Path Data="{StaticResource ErrorIcon}" 
              Fill="{DynamicResource ErrorForegroundBrush}" />
        
        <TextBlock Grid.Column="1" 
                   Text="{Binding ErrorMessage}"
                   Foreground="{DynamicResource ErrorForegroundBrush}"
                   Margin="8,0" />
        
        <Button Grid.Column="2" 
                Command="{Binding DismissErrorCommand}"
                Content="✕"
                Style="{StaticResource IconButton}" />
    </Grid>
</Border>
```

## Best Practices

| Practice | Reason |
|----------|--------|
| Log before showing user message | Capture full context for debugging |
| Include correlation ID | Enables support to find logs |
| Sanitize sensitive data | Protect user privacy in reports |
| Isolate widget errors | Don't let plugins crash the host |
| Use structured exception types | Easier to handle specifically |
| Provide actionable messages | Help users resolve issues |

## Anti-Patterns

| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| `catch { }` | Swallows all errors silently | At minimum log the exception |
| Showing stack traces | Confuses users, security risk | Map to friendly messages |
| Throwing from exception handlers | Recursive failure | Always catch in handlers |
| Generic error messages only | User can't act on them | Be specific when possible |

## References

- `references/global-handlers.md` for exception hooks
- `references/ui-errors.md` for UI surface patterns
- `references/crash-reporting.md` for report capture and storage