All guides
No guide matches your search.
Fallback
Give one operation a replacement value for when the call fails. Choose whether the fallback may also replace refusals, and see what it never replaces.
On this page
The code blocks come from examples/09-choosing-policies.php (opens in a new tab). The longer annotated script is examples/04-fallback.php (opens in a new tab).
A fallback belongs to one operation
A fallback is a FallbackOperation that you attach to a single operation. You pass it in the fallback: argument of newContext(). It receives the original Outcome and returns the replacement value. A pipeline whose context has no fallback never replaces anything.
/** @implements FallbackOperation<string> */
final class CachedCatalog implements FallbackOperation
{
public function invoke(Outcome $original, ExecutionContext $context): string
{
return 'cached catalog';
}
}Two things must be true before a fallback replaces a failure. The operation carries a fallback, and your classifier marked the exception as fallback-eligible with ExceptionClassifier::fallbackOn(). The default is none. Reqtide returns every exception you did not list.
Choose what it may replace
By default a fallback replaces only what your callable did. With FallbackScope::IncludeAdmissionRefusals it may also replace a refusal by the circuit breaker, bulkhead, rate limit or cooldown.
Every pipeline in this guide starts from the same two required options:
$base = static fn(): PipelineBuilder => PipelineBuilder::profile('choosing', 'v1')
->partition('shop', 'prod', 'catalog-api')
->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative);
$ok = static fn(): string => 'ok';
$context = static fn(CompiledPipeline $pipeline): ExecutionContext => $pipeline->newContext(Duration::seconds(30));This snippet spends the only rate-limit token. Then it makes a second call under each scope:
echo "-- 5. Fallback: replace failures, and optionally refusals\n";
$build = static fn(FallbackScope $scope): CompiledPipeline => $base()
->rateLimit(PipelineBuilder::tokenBucket(1, 1, Duration::seconds(3600)))
->fallbackScope($scope)
->compileInMemory(ExceptionClassifier::create()->fallbackOn(\Throwable::class));
foreach ([FallbackScope::BusinessOutcomesOnly, FallbackScope::IncludeAdmissionRefusals] as $scope) {
$pipeline = $build($scope);
$pipeline->execute($ok, $pipeline->newContext(Duration::seconds(30))); // uses the only token
try {
$value = $pipeline->execute($ok, $pipeline->newContext(Duration::seconds(30), fallback: new CachedCatalog()));
echo "{$scope->value}: second call returned '{$value}'\n";
} catch (AdmissionRejectedException | AdmissionDeferredException $e) {
echo "{$scope->value}: second call refused with {$e->reason()->value}\n";
}
}-- 5. Fallback: replace failures, and optionally refusals
business_outcomes_only: second call refused with rate.exhausted
include_admission_refusals: second call returned 'cached catalog'The default scope lets the refusal through, so you still know that you are being throttled. With IncludeAdmissionRefusals the second call returns the cached value. The refusal is not lost: detailed mode keeps it as policyCause. A fallback that hides refusals also hides the throttling, so choose this scope on purpose.
fallbackScope(IncludeAdmissionRefusals) needs at least one admission policy: a rate limit, circuit breaker, bulkhead or cooldown. The builder rejects the combination otherwise.
What a fallback never replaces
- A deadline. If the total deadline passed, the result is
deadline_exceeded. - A cancellation. This also holds while the fallback runs. A cancellation requested during the fallback makes the result
cancelled, and detailed mode still holds the value or exception that the fallback produced. A deadline that passes during the fallback wins over the cancellation. - An infrastructure failure, such as an unreachable state backend. Reqtide does not invent behavior when its own state is down.
A fallback shares the total deadline of the operation. It runs inside the same budget as the attempts before it.
When the fallback itself fails
A fallback that throws is reported as FallbackFailedException in simple mode. The exception of the fallback is available as getPrevious(). In detailed mode the disposition is fallback_failed. The original failure stays on the exception. The annotated script 04-fallback.php shows both.
Testing a fallback
Script the failing attempts, then assert on the dispatch count and the recorded events. Testing with the virtual clock shows how.
Was this page helpful?