FallbackStrategy
The FallbackStrategy
provides alternative responses when operations fail, ensuring graceful degradation and improved user experience.
Overview
The fallback strategy executes alternative logic when the primary operation fails. This enables graceful degradation by providing cached data, default values, or alternative service responses.
class FallbackStrategy<T> extends ResilienceStrategy<T> {
FallbackStrategy(FallbackStrategyOptions<T> options);
Future<Outcome<T>> executeCore<T>(
ResilienceCallback<T> callback,
ResilienceContext context,
ResilienceCallback<T> next,
);
}
class FallbackStrategyOptions<T> {
const FallbackStrategyOptions({
required this.fallbackAction,
this.shouldHandle,
this.onFallback,
});
}
FallbackStrategyOptions Properties
fallbackAction
The action to execute when the primary operation fails.
Type: FallbackAction<T>
Required: Yes
FallbackStrategyOptions<String>(
fallbackAction: (context, args) async => 'fallback-value',
)
shouldHandle
Predicate to determine which outcomes should trigger the fallback.
Type: ShouldHandlePredicate<T>?
Default: null
(handles all exceptions)
FallbackStrategyOptions<String>(
shouldHandle: (outcome) =>
outcome.hasException &&
outcome.exception is SocketException,
fallbackAction: (context, args) async => 'offline-mode',
)
onFallback
Callback invoked when the fallback action is executed.
Type: OnFallbackCallback<T>?
Default: null
FallbackStrategyOptions<String>(
fallbackAction: (context, args) async => 'fallback',
onFallback: (context, args) {
logger.info('Fallback executed due to: ${args.outcome.exception}');
},
)
Callback Types
FallbackAction<T>
typedef FallbackAction<T> = Future<T> Function(
ResilienceContext context,
FallbackActionArgs<T> args,
);
Defines the fallback logic to execute when the primary operation fails.
FallbackActionArgs Properties:
outcome
- The failed outcome that triggered the fallbackcancellationToken
- Token for cancellation support
ShouldHandlePredicate<T>
typedef ShouldHandlePredicate<T> = bool Function(Outcome<T> outcome);
Determines whether an outcome should trigger the fallback action.
OnFallbackCallback<T>
typedef OnFallbackCallback<T> = void Function(
ResilienceContext context,
OnFallbackArgs<T> args,
);
Called when the fallback action is executed.
OnFallbackArgs Properties:
outcome
- The failed outcome that triggered the fallback
Usage Examples
Basic Fallback
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<String>(
fallbackAction: (context, args) async => 'Service temporarily unavailable',
));
final pipeline = ResiliencePipelineBuilder()
.addStrategy(fallbackStrategy)
.build();
Cached Data Fallback
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<UserData>(
shouldHandle: (outcome) => outcome.hasException,
fallbackAction: (context, args) async {
final userId = context.getProperty<String>('userId');
final cachedData = await cache.get('user_$userId');
if (cachedData != null) {
return UserData.fromJson(cachedData);
}
// Return default user data if no cache
return UserData.defaultUser();
},
onFallback: (context, args) {
logger.info('Using cached user data due to service failure');
},
));
Network Fallback to Local Storage
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<List<Article>>(
shouldHandle: (outcome) {
if (!outcome.hasException) return false;
final exception = outcome.exception;
return exception is SocketException ||
exception is TimeoutException ||
(exception is HttpException && exception.statusCode >= 500);
},
fallbackAction: (context, args) async {
logger.warn('Network failed, loading from local storage');
return await localStorage.getArticles();
},
onFallback: (context, args) {
metrics.incrementCounter('fallback_to_local_storage');
},
));
Multi-Level Fallback
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<String>(
fallbackAction: (context, args) async {
// Try secondary service first
try {
return await secondaryService.getData();
} catch (e) {
logger.warn('Secondary service also failed, using cache');
// Fall back to cache
final cached = await cache.get('data');
if (cached != null) {
return cached;
}
// Final fallback to default value
return 'Default data - all services unavailable';
}
},
onFallback: (context, args) {
final primaryError = args.outcome.exception;
logger.error('Primary service failed: $primaryError');
},
));
Conditional Fallback
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<PaymentResult>(
shouldHandle: (outcome) {
if (!outcome.hasException) return false;
final exception = outcome.exception;
// Only fallback for network issues, not validation errors
return exception is SocketException ||
exception is TimeoutException;
},
fallbackAction: (context, args) async {
// Queue payment for later processing
final paymentData = context.getProperty<PaymentData>('paymentData');
await paymentQueue.add(paymentData);
return PaymentResult.queued('Payment queued for processing');
},
onFallback: (context, args) {
logger.info('Payment queued due to network issues');
notificationService.notifyUser('Payment will be processed shortly');
},
));
Degraded Service Fallback
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<SearchResults>(
fallbackAction: (context, args) async {
final query = context.getProperty<String>('searchQuery');
// Use simpler search algorithm as fallback
final results = await simplifiedSearch.search(query);
return SearchResults(
items: results,
isLimitedResults: true,
message: 'Limited search results due to service issues',
);
},
onFallback: (context, args) {
logger.warn('Using degraded search service');
metrics.incrementCounter('degraded_search_fallback');
},
));
Fallback with User Context
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<Recommendations>(
fallbackAction: (context, args) async {
final userId = context.getProperty<String>('userId');
final userPreferences = context.getProperty<UserPreferences>('preferences');
// Generate generic recommendations based on user preferences
return await generateGenericRecommendations(userPreferences);
},
onFallback: (context, args) {
final userId = context.getProperty<String>('userId');
logger.info('Providing generic recommendations for user $userId');
},
));
// Usage with context
final context = ResilienceContext();
context.setProperty('userId', 'user123');
context.setProperty('preferences', userPreferences);
final recommendations = await pipeline.execute(
(context) => recommendationService.getPersonalizedRecommendations(userId),
context: context,
);
Error-Specific Fallbacks
final fallbackStrategy = FallbackStrategy(FallbackStrategyOptions<ApiResponse>(
fallbackAction: (context, args) async {
final exception = args.outcome.exception;
if (exception is TimeoutException) {
return ApiResponse.timeout('Request timed out, please try again');
}
if (exception is HttpException) {
switch (exception.statusCode) {
case 404:
return ApiResponse.notFound('Resource not found');
case 429:
return ApiResponse.rateLimited('Too many requests, please wait');
case 503:
return ApiResponse.serviceUnavailable('Service temporarily unavailable');
default:
return ApiResponse.error('Service error occurred');
}
}
return ApiResponse.error('An unexpected error occurred');
},
onFallback: (context, args) {
final exception = args.outcome.exception;
logger.warn('API fallback triggered by: ${exception.runtimeType}');
},
));
Circuit Breaker with Fallback
final pipeline = ResiliencePipelineBuilder()
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 10,
))
.addFallback(FallbackStrategyOptions<String>(
shouldHandle: (outcome) =>
outcome.hasException &&
outcome.exception is CircuitBreakerOpenException,
fallbackAction: (context, args) async {
return 'Service is currently experiencing issues. Please try again later.';
},
onFallback: (context, args) {
logger.warn('Circuit breaker is open, using fallback response');
},
))
.build();
Retry with Fallback
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addFallback(FallbackStrategyOptions<String>(
fallbackAction: (context, args) async {
logger.error('All retry attempts failed, using fallback');
return await getFromBackupService();
},
))
.build();
Best Practices
Provide Meaningful Fallbacks
// Good - Provides actual value
FallbackStrategyOptions<UserProfile>(
fallbackAction: (context, args) async {
return UserProfile.guest(); // Actual usable profile
},
)
// Avoid - Just returns error message
FallbackStrategyOptions<UserProfile>(
fallbackAction: (context, args) async {
throw Exception('Service unavailable'); // Not helpful
},
)
Use Context for Personalized Fallbacks
FallbackStrategyOptions<String>(
fallbackAction: (context, args) async {
final userLevel = context.getProperty<String>('userLevel');
final locale = context.getProperty<String>('locale');
return getLocalizedFallbackMessage(locale, userLevel);
},
)
Handle Fallback Failures
FallbackStrategyOptions<String>(
fallbackAction: (context, args) async {
try {
return await backupService.getData();
} catch (e) {
// Even fallback can fail - provide ultimate fallback
logger.error('Backup service also failed: $e');
return 'System temporarily unavailable';
}
},
)
Monitor Fallback Usage
FallbackStrategyOptions<String>(
onFallback: (context, args) {
final operation = context.getProperty<String>('operation');
final exception = args.outcome.exception;
// Track fallback metrics
metrics.incrementCounter('fallback_executed', {
'operation': operation,
'exception_type': exception.runtimeType.toString(),
});
// Alert if fallbacks are frequent
if (fallbackRateExceeded(operation)) {
alertService.sendAlert('High fallback rate for $operation');
}
},
)
Keep Fallbacks Simple
// Good - Simple and fast
FallbackStrategyOptions<List<Item>>(
fallbackAction: (context, args) async => <Item>[],
)
// Avoid - Complex fallback that might also fail
FallbackStrategyOptions<List<Item>>(
fallbackAction: (context, args) async {
// Complex logic that might introduce new failure points
return await complexFallbackLogic();
},
)
Test Fallback Scenarios
// Ensure fallbacks are tested
test('should use cached data when service fails', () async {
// Arrange
when(mockService.getData()).thenThrow(SocketException('Network error'));
when(mockCache.get('data')).thenReturn('cached-data');
// Act
final result = await pipeline.execute((context) => mockService.getData());
// Assert
expect(result, equals('cached-data'));
});