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