Error Handling

The Supa Architecture framework provides a comprehensive error handling system designed to capture, categorize, and report errors across the entire application stack. The system ensures robust error management while maintaining application stability and providing meaningful feedback to users.

Purpose and Scope

The error handling system serves several critical functions:

  • Structured Error Management: Consistent error representation across the application
  • Global Error Tracking: Centralized error capture and reporting
  • User Experience: Graceful error handling with meaningful user feedback
  • Development Support: Detailed error information for debugging and monitoring
  • Integration: Seamless integration with external error reporting services

Error Handling Architecture

The framework implements a layered error handling approach:

Application Layer (UI)
       ↓
Business Logic Layer (BLoCs)
       ↓
Repository Layer
       ↓
Data Access Layer (API/Storage)
       ↓
External Services

Each layer handles errors appropriate to its scope while allowing critical errors to bubble up to higher layers.

Core Error Components

SupaException Hierarchy

The framework defines a structured exception hierarchy for consistent error handling:

abstract class SupaException implements Exception {
  final String message;
  final String? code;
  final dynamic originalError;
  final StackTrace? stackTrace;
  
  const SupaException({
    required this.message,
    this.code,
    this.originalError,
    this.stackTrace,
  });
}

Specific Exception Types

Network Exceptions

class NetworkException extends SupaException {
  final int? statusCode;
  final String? endpoint;
  
  const NetworkException({
    required String message,
    this.statusCode,
    this.endpoint,
  });
}

class TimeoutException extends NetworkException {
  const TimeoutException({
    String message = 'Request timed out',
    String? endpoint,
  }) : super(message: message, endpoint: endpoint, code: 'TIMEOUT');
}

class NoInternetException extends NetworkException {
  const NoInternetException({
    String message = 'No internet connection',
  }) : super(message: message, code: 'NO_INTERNET');
}

Authentication Exceptions

class AuthenticationException extends SupaException {
  const AuthenticationException({
    required String message,
    String? code,
  });
}

class TokenExpiredException extends AuthenticationException {
  const TokenExpiredException({
    String message = 'Authentication token has expired',
  }) : super(message: message, code: 'TOKEN_EXPIRED');
}

class UnauthorizedException extends AuthenticationException {
  const UnauthorizedException({
    String message = 'Unauthorized access',
  }) : super(message: message, code: 'UNAUTHORIZED');
}

Validation Exceptions

class ValidationException extends SupaException {
  final Map<String, List<String>> fieldErrors;
  
  const ValidationException({
    required String message,
    required this.fieldErrors,
    String? code,
  });
}

class BusinessRuleException extends SupaException {
  const BusinessRuleException({
    required String message,
    String? code,
  });
}

Global Error Handling

ErrorHandlingBloc

The framework includes a dedicated BLoC for global error management:

class ErrorHandlingBloc extends Bloc<ErrorHandlingEvent, ErrorHandlingState> {
  final ErrorReportingService _errorReportingService;
  final Logger _logger;
  
  ErrorHandlingBloc({
    required ErrorReportingService errorReportingService,
    required Logger logger,
  }) : super(ErrorHandlingInitial()) {
    on<ReportError>(_onReportError);
  }
  
  Future<void> _onReportError(
    ReportError event,
    Emitter<ErrorHandlingState> emit,
  ) async {
    // Log error locally
    _logger.error(
      event.error.message,
      error: event.error.originalError,
      stackTrace: event.error.stackTrace,
    );
    
    // Report to external services
    await _errorReportingService.reportError(
      event.error,
      context: event.context,
      userId: event.userId,
    );
  }
}

Error Reporting Integration

Sentry Integration

class SentryErrorReportingService implements ErrorReportingService {
  @override
  Future<void> initialize({required String dsn}) async {
    await SentryFlutter.init(
      (options) {
        options.dsn = dsn;
        options.environment = Environment.current;
        options.tracesSampleRate = 0.1;
      },
    );
  }
  
  @override
  Future<void> reportError(
    SupaException error, {
    Map<String, dynamic>? context,
    String? userId,
  }) async {
    await Sentry.captureException(
      error,
      stackTrace: error.stackTrace,
      withScope: (scope) {
        if (userId != null) {
          scope.setUser(SentryUser(id: userId));
        }
        
        if (context != null) {
          scope.setContexts('additional_context', context);
        }
        
        scope.setTag('error_code', error.code ?? 'unknown');
        scope.setLevel(SentryLevel.error);
      },
    );
  }
}

Firebase Crashlytics Integration

class FirebaseCrashlyticsService implements ErrorReportingService {
  @override
  Future<void> initialize() async {
    await Firebase.initializeApp();
    await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
    
    FlutterError.onError = (FlutterErrorDetails details) {
      FirebaseCrashlytics.instance.recordFlutterFatalError(details);
    };
  }
  
  @override
  Future<void> reportError(
    SupaException error, {
    Map<String, dynamic>? context,
    String? userId,
  }) async {
    if (userId != null) {
      await FirebaseCrashlytics.instance.setUserIdentifier(userId);
    }
    
    await FirebaseCrashlytics.instance.recordError(
      error,
      error.stackTrace,
      reason: error.message,
    );
  }
}

Layer-Specific Error Handling

API Layer Error Handling

class ApiClient {
  Future<HttpResponse<T>> request<T>({
    required String method,
    required String path,
  }) async {
    try {
      final response = await _dio.request(path);
      return HttpResponse.success(response.data);
    } on DioException catch (e) {
      final supaException = _mapDioException(e);
      return HttpResponse.error(supaException);
    }
  }
  
  SupaException _mapDioException(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        return TimeoutException(endpoint: e.requestOptions.path);
      case DioExceptionType.connectionError:
        return NoInternetException();
      default:
        return NetworkException(message: e.message ?? 'Unknown error');
    }
  }
}

Repository Layer Error Handling

abstract class BaseRepository {
  Future<Result<T>> safeCall<T>(
    Future<T> Function() operation, {
    String? operationName,
  }) async {
    try {
      final result = await operation();
      return Result.success(result);
    } on SupaException catch (e) {
      return Result.error(e);
    } catch (e, stackTrace) {
      final supaException = SupaException(
        message: 'Repository operation failed: ${e.toString()}',
        originalError: e,
        stackTrace: stackTrace,
      );
      return Result.error(supaException);
    }
  }
}

UI Error Handling

Error Display Components

class ErrorMessage extends StatelessWidget {
  final String message;
  final String? actionLabel;
  final VoidCallback? onAction;
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.errorContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        children: [
          Icon(
            Icons.error_outline,
            color: Theme.of(context).colorScheme.error,
          ),
          SizedBox(height: 8),
          Text(message),
          if (actionLabel != null && onAction != null) ...[
            SizedBox(height: 12),
            ElevatedButton(
              onPressed: onAction,
              child: Text(actionLabel!),
            ),
          ],
        ],
      ),
    );
  }
}

Global Error Handler

class GlobalErrorHandler extends StatelessWidget {
  final Widget child;
  
  @override
  Widget build(BuildContext context) {
    return BlocListener<ErrorHandlingBloc, ErrorHandlingState>(
      listener: (context, state) {
        if (state is ErrorHandlingFailure) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('An error occurred: ${state.message}'),
              backgroundColor: Theme.of(context).colorScheme.error,
            ),
          );
        }
      },
      child: child,
    );
  }
}

Best Practices

1. Error Context

Always provide meaningful context when reporting errors:

void reportErrorWithContext(SupaException error) {
  final context = {
    'user_id': currentUser?.id,
    'screen': currentRoute,
    'timestamp': DateTime.now().toIso8601String(),
    'app_version': packageInfo.version,
    'platform': Platform.operatingSystem,
  };
  
  errorBloc.add(ReportError(error: error, context: context));
}

2. Error Recovery

class ErrorRecoveryService {
  static Future<bool> attemptRecovery(SupaException error) async {
    switch (error.code) {
      case 'TOKEN_EXPIRED':
        return await _refreshToken();
      case 'NO_INTERNET':
        return await _waitForConnection();
      default:
        return false;
    }
  }
}

Previous: State Management View on GitHub