Skip to content

Graceful AI Fallback

Graceful AI Fallback is a resilience pattern for AI-enhanced features: every AI operation is bounded by a short timeout, and failure (timeout, provider error, incomprehensible response) silently falls back to a deterministic baseline. The user experience degrades gracefully — from AI-enhanced to standard — rather than failing with an error.

The pattern is the AI-specific application of the Circuit Breaker and Retry pattern: when the AI “circuit” opens, the fallback path takes over without bubbling an exception to the caller.

flowchart TD
    CALL[Feature request] --> AI[AI operation\nwith timeout]
    AI -- Success\n≤ timeout --> ENHANCED[AI-enhanced result]
    AI -- Timeout\nor error --> FALLBACK{Fallback type}
    FALLBACK -- Heuristic --> HEUR[Deterministic\nheuristic result]
    FALLBACK -- Pass-through --> PASS[Raw input\nas-is]
    FALLBACK -- Null / skip --> SKIP[Feature skipped\nsilently]
    ENHANCED --> USER[User sees result]
    HEUR --> USER
    PASS --> USER
    SKIP --> USER

The pattern appears consistently in all cross-cutting *.AI packages. Two timeout tiers are used depending on user-facing latency sensitivity:

PackageTimeoutFallback
Granit.Querying.AI2 sPass phrase as QueryRequest.Search (full-text)
Granit.DataExchange.AI5 sHeuristic IMappingSuggestionService
Granit.Validation.AI1 sSkip AI check, return valid
Granit.Notifications.AI3 sUse static template
Granit.Privacy.AI5 sConservative: mark field as potentially containing PII
internal sealed class AINaturalLanguageQueryTranslator(
IAIChatClientFactory factory,
ILogger<AINaturalLanguageQueryTranslator> logger)
: INaturalLanguageQueryTranslator
{
public async Task<QueryRequest?> TranslateAsync(
string phrase, QueryDefinition definition, CancellationToken ct)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(2));
try
{
var workspace = await factory.CreateAsync("default", cts.Token);
// ... build prompt from QueryDefinition metadata ...
var response = await workspace.Chat.CompleteAsync<QueryRequest>(
messages, cancellationToken: cts.Token);
return response.Result;
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
// Timeout — not user cancellation
logger.AIQueryTranslationTimeout(phrase);
return null; // caller falls back to QueryRequest.Search = phrase
}
catch (Exception ex)
{
logger.AIQueryTranslationError(phrase, ex);
return null;
}
}
}
internal sealed class AIMappingSuggestionService(
IAIChatClientFactory factory,
IMappingSuggestionService heuristic, // injected baseline
ILogger<AIMappingSuggestionService> logger)
: IMappingSuggestionService
{
public async Task<MappingSuggestion[]> SuggestAsync(
ImportPreview preview, ImportDefinition definition, CancellationToken ct)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5));
try
{
return await SuggestWithAIAsync(preview, definition, cts.Token);
}
catch (Exception ex) when (ex is not OperationCanceledException { CancellationToken: var c }
|| c == cts.Token)
{
logger.AIMappingFallback(ex.Message);
return await heuristic.SuggestAsync(preview, definition, ct);
}
}
}
  1. Short, separate CancellationTokenSource — always linked to the caller’s token so user cancellation still propagates, but the AI timeout is independent.
  2. Distinguish timeout from user cancellation — check !ct.IsCancellationRequested before swallowing OperationCanceledException.
  3. Log at Warning, never Error — a fallback is expected behavior, not a failure.
  4. Return null or the baseline, never throw — the caller controls the user experience after a fallback.
FileRole
src/Granit.Querying.AI/AINaturalLanguageQueryTranslator.cs2s timeout + null fallback
src/Granit.DataExchange.AI/AIMappingSuggestionService.cs5s timeout + heuristic fallback
src/Granit.Validation.AI/AIContentModerationValidator.cs1s timeout + pass-through
ProblemGraceful AI Fallback solution
LLM latency spikes block user requestsHard timeout prevents unbounded wait
Provider outage breaks the featureFallback ensures feature remains usable
AI error propagates as 500 to userException swallowed, baseline returned
Debugging silenced errorsWarning log with full context for observability