Exception Handling

Comprehensive exception handling with automatic response formatting, reporting, and context management.

Error HandlingResponse FormattingError ReportingContext TrackingMultiple Formats

Quick Start

Basic Exception Handling

dart
// Automatic exception handling in controllers
class UserController {
  Future<void> createUser(Request request) async {
    try {
      final userData = request.validate({
        'name': 'required|string|min:2',
        'email': 'required|email|unique:users',
        'password': 'required|string|min:8',
      });

      final user = await User.create(userData);
      return response.json({'user': user});
    } catch (e) {
      // ExceptionHandler will automatically handle this
      // and send appropriate error response
      rethrow;
    }
  }
}

// Manual exception handling
try {
  await riskyOperation();
} catch (e, stackTrace) {
  ExceptionHandler.handle(response, e, stackTrace);
}

💡 Note: Exception handling is automatically configured via service provider

⚡ Tip: Use AppException for custom application errors with specific status codes

Custom Exceptions

dart
// Base custom exception
class ValidationException extends AppException {
  ValidationException(String message, {Map<String, dynamic>? errors})
      : super(message, statusCode: 400, details: errors);

  @override
  Map<String, dynamic> toResponse() => {
        'error': 'validation_failed',
        'message': message,
        'errors': details,
        'status_code': statusCode,
      };
}

// Specific validation errors
class UnauthorizedException extends AppException {
  UnauthorizedException([String message = 'Unauthorized'])
      : super(message, statusCode: 401);
}

class NotFoundException extends AppException {
  NotFoundException(String resource)
      : super('$resource not found', statusCode: 404);
}

class ConflictException extends AppException {
  ConflictException(String message)
      : super(message, statusCode: 409);
}

// Usage in business logic
class UserService {
  Future<User> createUser(Map<String, dynamic> data) async {
    if (await User.exists(data['email'])) {
      throw ConflictException('User with this email already exists');
    }

    if (!isValidPassword(data['password'])) {
      throw ValidationException('Password does not meet requirements', {
        'password': ['Must be at least 8 characters', 'Must contain numbers']
      });
    }

    return await User.create(data);
  }
}

Common Exception Types

  • ValidationException - Input validation errors (400)
  • AuthenticationException - Auth failures (401)
  • AuthorizationException - Permission denied (403)
  • NotFoundException - Resource not found (404)
  • ConflictException - Resource conflicts (409)
  • RateLimitException - Too many requests (429)

Exception Handler

dart
// Basic exception handling
try {
  await processPayment(order);
} catch (e, stackTrace) {
  ExceptionHandler.handle(response, e, stackTrace);
}

// Handle with specific format
try {
  await generateReport();
} catch (e, stackTrace) {
  ExceptionHandler.handleWithFormat(response, e, 'xml', stackTrace);
}

// Custom response handling
ExceptionHandler.configure(
  showDetailedErrors: Khadem.isDevelopment,
  includeStackTracesInResponse: Khadem.isDevelopment,
  customFormatter: (AppException error) => {
    'success': false,
    'error': {
      'code': error.statusCode,
      'type': error.runtimeType.toString(),
      'message': error.message,
      'details': error.details,
    },
    'timestamp': DateTime.now().toIso8601String(),
  },
);

Handler Features

  • • Automatic JSON response formatting
  • • Development vs production error details
  • • Multiple response formats (JSON, XML, HTML)
  • • Custom error response formatting
  • • Request context inclusion

Response Formats

json
{
  "error": true,
  "message": "Validation failed",
  "status_code": 400,
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {
    "email": ["Email is required"],
    "password": ["Password must be at least 8 characters"]
  },
  "exception_type": "ValidationException"
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<error>
  <status_code>400</status_code>
  <message>Validation failed</message>
  <details>
    <email>Email is required</email>
    <password>Password must be at least 8 characters</password>
  </details>
  <timestamp>2024-01-15T10:30:00.000Z</timestamp>
</error>
dart
// Automatic format detection based on Accept header
app.get('/api/users', (request, response) async {
  try {
    final users = await User.all();
    return response.json({'users': users});
  } catch (e, stackTrace) {
    // Will respond in JSON, XML, or HTML based on request
    ExceptionHandler.handle(response, e, stackTrace);
  }
});

// Force specific format
app.get('/api/users.xml', (request, response) async {
  try {
    final users = await User.all();
    return response.xml({'users': users});
  } catch (e, stackTrace) {
    ExceptionHandler.handleWithFormat(response, e, 'xml', stackTrace);
  }
});

Exception Reporter

dart
// Automatic reporting for AppException
try {
  await processOrder(orderData);
} catch (e, stackTrace) {
  if (e is AppException) {
    ExceptionReporter.reportAppException(e, stackTrace, {
      'order_id': orderData['id'],
      'user_id': orderData['user_id'],
      'amount': orderData['total'],
    });
  } else {
    ExceptionReporter.reportException(e, stackTrace, {
      'operation': 'process_order',
      'order_data': orderData,
    });
  }
  rethrow;
}

// Report with custom severity
ExceptionReporter.reportWithLevel('critical', error, stackTrace, {
  'service': 'payment_processor',
  'impact': 'high',
});

// Configure reporting
ExceptionReporter.configure(
  includeStackTraces: true,
  includeUserContext: true,
  includeRequestContext: true,
  includeEnvironmentInfo: true,
  minimumReportLevel: 'warning',
  globalContext: {
    'service': 'user_service',
    'version': '1.2.0',
  },
);

Reporting Features

  • • Automatic logging with context
  • • External service integration (Sentry, Rollbar)
  • • Request and user context tracking
  • • Environment information inclusion
  • • Custom global context

Configuration

dart
// Configure exception handler
ExceptionHandler.configure(
  showDetailedErrors: Khadem.isDevelopment,
  includeStackTracesInResponse: Khadem.isDevelopment,
  customFormatter: (AppException error) => {
    'success': false,
    'error': {
      'code': error.statusCode,
      'type': error.runtimeType.toString(),
      'message': error.message,
      'timestamp': DateTime.now().toIso8601String(),
    },
    'data': error.details,
  },
);

// Configure exception reporter
ExceptionReporter.configure(
  includeStackTraces: true,
  includeUserContext: true,
  includeRequestContext: true,
  includeEnvironmentInfo: true,
  minimumReportLevel: Khadem.isProduction ? 'error' : 'warning',
);

// Add global context for all reports
ExceptionReporter.addGlobalContext('service', 'api_gateway');
ExceptionReporter.addGlobalContext('version', '2.1.0');
ExceptionReporter.addGlobalContext('environment', Khadem.environment);

Configuration Options

  • showDetailedErrors - Show error details in development
  • includeStackTracesInResponse - Include stack traces in responses
  • customFormatter - Custom error response formatting
  • includeStackTraces - Include stack traces in reports
  • includeUserContext - Include user information in reports
  • includeRequestContext - Include request details in reports

Error Handling Patterns

Try-Catch with Custom Exceptions

Handle errors with specific exception types

dart
// Specific exception handling
class PaymentService {
  Future<void> processPayment(Order order) async {
    try {
      // Validate payment data
      if (order.total <= 0) {
        throw ValidationException('Order total must be greater than 0');
      }

      // Check payment method
      if (!isValidPaymentMethod(order.paymentMethod)) {
        throw ValidationException('Invalid payment method');
      }

      // Process payment
      await _chargeCard(order);

    } on ValidationException catch (e) {
      // Handle validation errors
      ExceptionReporter.reportAppException(e, null, {
        'order_id': order.id,
        'validation_errors': e.details,
      });
      throw e; // Re-throw for controller to handle

    } on PaymentException catch (e) {
      // Handle payment processing errors
      ExceptionReporter.reportAppException(e, null, {
        'order_id': order.id,
        'payment_method': order.paymentMethod,
      });
      throw PaymentFailedException('Payment processing failed');

    } catch (e, stackTrace) {
      // Handle unexpected errors
      ExceptionReporter.reportException(e, stackTrace, {
        'order_id': order.id,
        'operation': 'process_payment',
      });
      throw InternalServerException('An unexpected error occurred');
    }
  }
}

Middleware Error Handling

Handle errors in middleware pipeline

dart
// Error handling middleware
class ErrorHandlingMiddleware extends Middleware {
  @override
  Future<void> handle(Request request, Response response, Next next) async {
    try {
      await next();
    } on AppException catch (e, stackTrace) {
      ExceptionReporter.reportAppException(e, stackTrace, {
        'request_method': request.method,
        'request_url': request.uri.toString(),
        'user_agent': request.headers.header('user-agent'),
        'ip_address': request.ip,
      });

      ExceptionHandler.handle(response, e, stackTrace);
    } catch (e, stackTrace) {
      ExceptionReporter.reportException(e, stackTrace, {
        'request_method': request.method,
        'request_url': request.uri.toString(),
        'user_agent': request.headers.header('user-agent'),
        'ip_address': request.ip,
      });

      ExceptionHandler.handle(response, e, stackTrace);
    }
  }
}

// Register middleware
class Kernel {
  List<Middleware> get middleware => [
    ErrorHandlingMiddleware(),
    // Other middleware...
  ];
}

Async Error Handling

Handle errors in async operations

dart
// Async error handling
class AsyncService {
  Future<void> performAsyncOperation() async {
    final completer = Completer<void>();

    // Start async operation
    Timer(Duration(seconds: 5), () async {
      try {
        await riskyAsyncOperation();
        completer.complete();
      } catch (e, stackTrace) {
        completer.completeError(e, stackTrace);
      }
    });

    try {
      await completer.future;
    } catch (e, stackTrace) {
      // Handle async errors
      ExceptionReporter.reportException(e, stackTrace, {
        'operation': 'async_operation',
        'timestamp': DateTime.now(),
      });
      throw AsyncOperationException('Async operation failed');
    }
  }

  // Using Future.error for error propagation
  Future<Result> processWithError() async {
    try {
      final data = await fetchData();
      final result = await processData(data);
      return Result.success(result);
    } catch (e, stackTrace) {
      ExceptionReporter.reportException(e, stackTrace, {
        'operation': 'process_with_error',
      });
      return Result.failure(e);
    }
  }
}

Global Context

dart
// Add global context for all reports
ExceptionReporter.addGlobalContext('service', 'user_management');
ExceptionReporter.addGlobalContext('version', '1.3.2');
ExceptionReporter.addGlobalContext('environment', Khadem.environment);

// Add user context (typically in auth middleware)
class AuthMiddleware extends Middleware {
  @override
  Future<void> handle(Request request, Response response, Next next) async {
    final user = await authenticateUser(request);

    if (user != null) {
      ExceptionReporter.addGlobalContext('user_id', user.id);
      ExceptionReporter.addGlobalContext('user_role', user.role);
      ExceptionReporter.addGlobalContext('user_email', user.email);
    }

    ExceptionReporter.addGlobalContext('request_id', request.id);
    ExceptionReporter.addGlobalContext('ip_address', request.ip);

    try {
      await next();
    } finally {
      // Clean up request-specific context
      ExceptionReporter.removeGlobalContext('request_id');
      if (user == null) {
        ExceptionReporter.removeGlobalContext('user_id');
        ExceptionReporter.removeGlobalContext('user_role');
        ExceptionReporter.removeGlobalContext('user_email');
      }
    }
  }
}

// Add session context
ExceptionReporter.addGlobalContext('session_id', session.id);
ExceptionReporter.addGlobalContext('last_activity', DateTime.now());

// Clear all custom context
ExceptionReporter.clearGlobalContext();

Context Types

  • user - Current user information
  • request - HTTP request details
  • environment - Platform and environment info
  • custom - Application-specific data
  • session - Session and authentication data

External Service Integration

dart
// Sentry integration
class SentryReporter {
  static void initialize() {
    // Initialize Sentry SDK
    Sentry.init((options) {
      options.dsn = 'your-sentry-dsn';
      options.environment = Khadem.environment;
      options.release = '1.2.0';
    });
  }

  static void report(Object error, Map<String, dynamic> context, StackTrace? stackTrace) {
    Sentry.captureException(
      error,
      stackTrace: stackTrace,
      withScope: (scope) {
        // Add context to Sentry scope
        scope.setUser(SentryUser(
          id: context['user']?['id'],
          email: context['user']?['email'],
        ));

        scope.setTag('service', context['service'] ?? 'unknown');
        scope.setTag('environment', context['environment'] ?? 'unknown');

        if (context['request'] != null) {
          scope.setContext('request', context['request']);
        }
      },
    );
  }
}

// Extend ExceptionReporter
class ExtendedExceptionReporter extends ExceptionReporter {
  static void _sendToExternalService(
    Object error,
    Map<String, dynamic> context,
    StackTrace? stackTrace,
  ) {
    // Send to Sentry
    SentryReporter.report(error, context, stackTrace);

    // Send to Rollbar
    Rollbar.report(error, stackTrace: stackTrace, context: context);

    // Send to custom monitoring service
    MonitoringService.report(error, context, stackTrace);
  }
}

Supported Services

  • Sentry - Real-time error tracking and performance monitoring
  • Rollbar - Full-stack error monitoring and debugging
  • Bugsnag - Automated error monitoring and reporting
  • LogRocket - Session replay and error tracking
  • Airbrake - Error monitoring and alerting

Best Practices

✅ Do's

  • • Use specific exception types for different error scenarios
  • • Include meaningful error messages for API consumers
  • • Configure different error detail levels for development vs production
  • • Add relevant context to exception reports
  • • Use appropriate HTTP status codes for different error types
  • • Handle exceptions gracefully without exposing sensitive information
  • • Log exceptions with sufficient context for debugging
  • • Test error scenarios and exception handling
  • • Use custom formatters for consistent error responses
  • • Monitor and alert on critical application errors

❌ Don'ts

  • • Don't expose sensitive information in error messages
  • • Don't show detailed stack traces in production
  • • Don't catch exceptions without proper handling or rethrowing
  • • Don't use generic exceptions for specific error scenarios
  • • Don't ignore exceptions or use empty catch blocks
  • • Don't log sensitive data like passwords or tokens
  • • Don't rely on exception messages for user-facing content
  • • Don't forget to configure error reporting for production
  • • Don't use exceptions for normal flow control
  • • Don't expose internal system details in error responses

Common Exception Types

HTTP-Related Exceptions

dart
// HTTP-related exceptions
class UnauthorizedException extends AppException {
  UnauthorizedException([String message = 'Unauthorized'])
      : super(message, statusCode: 401);
}

class ForbiddenException extends AppException {
  ForbiddenException([String message = 'Forbidden'])
      : super(message, statusCode: 403);
}

class NotFoundException extends AppException {
  NotFoundException(String resource)
      : super('$resource not found', statusCode: 404);
}

class MethodNotAllowedException extends AppException {
  MethodNotAllowedException(String method)
      : super('Method $method not allowed', statusCode: 405);
}

class ConflictException extends AppException {
  ConflictException(String message)
      : super(message, statusCode: 409);
}

class UnprocessableEntityException extends AppException {
  UnprocessableEntityException(String message, Map<String, dynamic>? errors)
      : super(message, statusCode: 422, details: errors);
}

class TooManyRequestsException extends AppException {
  TooManyRequestsException([String message = 'Too many requests'])
      : super(message, statusCode: 429);
}

class InternalServerException extends AppException {
  InternalServerException([String message = 'Internal server error'])
      : super(message, statusCode: 500);
}

Business Logic Exceptions

dart
// Business logic exceptions
class InsufficientFundsException extends AppException {
  InsufficientFundsException(double required, double available)
      : super(
          'Insufficient funds. Required: $required, Available: $available',
          statusCode: 400,
          details: {'required': required, 'available': available},
        );
}

class ProductOutOfStockException extends AppException {
  ProductOutOfStockException(String productId, int requested, int available)
      : super(
          'Product out of stock',
          statusCode: 409,
          details: {
            'product_id': productId,
            'requested': requested,
            'available': available,
          },
        );
}

class DuplicateOrderException extends AppException {
  DuplicateOrderException(String orderId)
      : super(
          'Duplicate order',
          statusCode: 409,
          details: {'order_id': orderId},
        );
}

class SubscriptionExpiredException extends AppException {
  SubscriptionExpiredException(DateTime expiryDate)
      : super(
          'Subscription expired',
          statusCode: 403,
          details: {'expiry_date': expiryDate.toIso8601String()},
        );
}

class FeatureNotAvailableException extends AppException {
  FeatureNotAvailableException(String feature, String plan)
      : super(
          'Feature not available in $plan plan',
          statusCode: 403,
          details: {'feature': feature, 'plan': plan},
        );
}

Validation Exceptions

dart
// Validation exceptions
class ValidationException extends AppException {
  ValidationException(String message, Map<String, List<String>>? errors)
      : super(message, statusCode: 400, details: errors);

  @override
  Map<String, dynamic> toResponse() => {
        'error': 'validation_failed',
        'message': message,
        'errors': details,
        'status_code': statusCode,
      };
}

// Specific validation errors
class RequiredFieldException extends ValidationException {
  RequiredFieldException(String field)
      : super('Field is required', {field: ['This field is required']});
}

class InvalidFormatException extends ValidationException {
  InvalidFormatException(String field, String expectedFormat)
      : super('Invalid format', {
          field: ['Expected format: $expectedFormat']
        });
}

class LengthException extends ValidationException {
  LengthException(String field, int min, int max, int actual)
      : super('Invalid length', {
          field: ['Length must be between $min and $max characters (current: $actual)']
        });
}

class RangeException extends ValidationException {
  RangeException(String field, num min, num max, num actual)
      : super('Value out of range', {
          field: ['Value must be between $min and $max (current: $actual)']
        });
}

// Usage in validation
class UserValidator {
  static void validateCreate(Map<String, dynamic> data) {
    final errors = <String, List<String>>{};

    if (data['name'] == null || data['name'].toString().isEmpty) {
      errors['name'] = ['Name is required'];
    } else if (data['name'].toString().length < 2) {
      errors['name'] = ['Name must be at least 2 characters'];
    }

    if (data['email'] == null || !isValidEmail(data['email'])) {
      errors['email'] = ['Valid email is required'];
    }

    if (errors.isNotEmpty) {
      throw ValidationException('Validation failed', errors);
    }
  }
}

Testing Exceptions

dart
// Unit test for custom exceptions
void main() {
  group('Custom Exceptions', () {
    test('ValidationException creates correct response', () {
      final errors = {'email': ['Invalid email format']};
      final exception = ValidationException('Validation failed', errors);

      expect(exception.statusCode, 400);
      expect(exception.message, 'Validation failed');
      expect(exception.details, errors);

      final response = exception.toResponse();
      expect(response['error'], 'validation_failed');
      expect(response['errors'], errors);
    });

    test('NotFoundException has correct status code', () {
      final exception = NotFoundException('User');
      expect(exception.statusCode, 404);
      expect(exception.message, 'User not found');
    });
  });

  group('Exception Handler', () {
    test('handles AppException correctly', () async {
      final exception = ValidationException('Test error', {});
      final response = MockResponse();

      ExceptionHandler.handle(response, exception);

      expect(response.statusCode, 400);
      expect(response.sentData['error'], true);
      expect(response.sentData['message'], 'Test error');
    });

    test('handles generic exception correctly', () async {
      final exception = Exception('Generic error');
      final response = MockResponse();

      ExceptionHandler.handle(response, exception);

      expect(response.statusCode, 500);
      expect(response.sentData['error'], true);
      expect(response.sentData['message'], 'Internal Server Error');
    });
  });

  group('Exception Reporter', () {
    test('includes context in reports', () async {
      ExceptionReporter.addGlobalContext('test', 'value');

      final exception = Exception('Test error');
      final context = {'custom': 'data'};

      // Mock logger to capture report
      final mockLogger = MockLogger();
      Khadem.logger = mockLogger;

      ExceptionReporter.reportException(exception, null, context);

      verify(mockLogger.error(
        any,
        context: captureAnyNamed('context'),
        stackTrace: anyNamed('stackTrace'),
      )).called(1);

      final capturedContext = verify(mockLogger.error(
        any,
        context: captureAnyNamed('context'),
      )).captured[0];

      expect(capturedContext['test'], 'value');
      expect(capturedContext['custom'], 'data');
      expect(capturedContext['exception']['type'], 'Exception');
    });
  });
}

Testing Strategies

  • • Test custom exception creation and properties
  • • Test exception handler response formatting
  • • Test exception reporter context inclusion
  • • Test error scenarios in controllers and services
  • • Test middleware error handling
  • • Test external service integration

On this page