Handling Failures
Reactive strategies — retry, circuit breaker, hedging, fallback — act on failures. Handling clauses tell them what a failure is.
The default
With no handling clause, the default is: any exception except OperationCanceledException. Cancellation isn't a fault; retrying it would fight the caller.
Exception clauses
var shield = Shield
.When<HttpRequestException>() // this exception type (and subtypes)
.Or<TimeoutExceededException>() // or this one
.OrWhen(ex => ex is IOException { Message: var m } && m.Contains("pipe")) // or any predicate
.Retry(5);
When<TException>()starts a clause matchingTExceptionand anything derived from it.When<TException>(predicate)narrows it further, andWhen(predicate)matches on any exception.Or<TException>()/Or<TException>(predicate)/OrWhen(predicate)add alternatives to the clause. All alternatives OR together.
Both builders speak the same grammar. When… and Or… are interchangeable — the convention is When for the first clause and Or for each addition: When<A>().Or<B>().OrWhen(...).
Result clauses
Sometimes failure isn't an exception — it's a well-formed response you don't like (an HTTP 500, an empty payload, a Status = "Retry" field). Lift into a typed shield with For<T> and add WhenResult:
var http = Shield.For<HttpResponseMessage>()
.When<HttpRequestException>()
.WhenResult(r => (int)r.StatusCode >= 500)
.Retry(3);
Now a 503 response triggers a retry exactly as a thrown HttpRequestException would. The delegate's return value is inspected — nothing is thrown internally, the outcome just counts as a failure.
Typed builders add result alternatives with OrResult(predicate) / OrResult(value), and two shorthands for the most common check of all:
Shield.For<User?>().WhenDefault().Retry(2); // retry when the result is null / default
// mid-chain: .OrDefault() adds the same check to an existing clause
Clauses are ambient
A clause applies to the strategy it creates and to every reactive strategy chained after it, until you write a new clause:
Shield
.When<HttpRequestException>() // clause #1
.Retry(3) // ← uses clause #1
.CircuitBreaker(5, breakDuration) // ← also clause #1
.When<TimeoutExceededException>() // clause #2 replaces #1 from here on
.Fallback(...); // ← uses clause #2
This is why most chains only need one clause, written once at the top — and why you never repeat a ShouldHandle predicate per strategy like in Polly v8.
Timeouts, rate limits and concurrency limits don't care why something failed — they act on time and concurrency, not outcomes. Clauses only drive the reactive strategies.
shield.For<T>() carries an ambient exception clause into the typed shield, and Shield.Compose(...) keeps the last input's clause ambient — so strategies chained afterwards keep handling what you declared. Write a new clause any time you want different handling.