Interview question
Retry an async operation with cancellation
Implements a bounded async retry helper that respects cancellation and does not retry every exception blindly.
TL;DR
Implements a bounded async retry helper that respects cancellation and does not retry every exception blindly.
Async/await, cancellation tokens, retry loops, exception filters, delays, and production-safe retry thinking.
Practice the problem like a real interview: restate, reason, implement, and test.
Implement RetryAsync<T> that calls an operation, retries transient failures up to a maximum number of attempts, waits between attempts, and passes the cancellation token through. The caller supplies isTransient.
Next in Coding Practice: LRU Cache
If the operation fails twice with transient exceptions and succeeds on the third call, return the successful value. If cancellation is requested during delay, throw OperationCanceledException.
isTransient returns false.I would use a for loop over attempts. Inside the loop, call the operation with the token. If it succeeds, return. If it throws and the exception is not transient, rethrow. If it is the last attempt, rethrow. Otherwise wait with Task.Delay(delay, cancellationToken). Passing the token into both the operation and delay is the key correctness point.
public static async Task<T> RetryAsync<T>(
Func<CancellationToken, Task<T>> operation,
Func<Exception, bool> isTransient,
int maxAttempts,
TimeSpan delay,
CancellationToken cancellationToken)
{
if (operation is null) throw new ArgumentNullException(nameof(operation));
if (isTransient is null) throw new ArgumentNullException(nameof(isTransient));
if (maxAttempts <= 0) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
if (delay < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(delay));
for (var attempt = 1; ; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
return await operation(cancellationToken);
}
catch (Exception exception) when (
exception is not OperationCanceledException &&
attempt < maxAttempts &&
isTransient(exception))
{
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, cancellationToken);
}
}
}
}
Time depends on operation cost and number of attempts. The helper itself uses O(1) space. Worst-case elapsed time includes operation duration plus delays between failed transient attempts.
OperationCanceledException.Spot a weak answer, missing edge case, or clearer explanation? Send it in.