C# HttpClient TaskCanceledException — Timeout Not an Error
Fix HttpClient TaskCanceledException by distinguishing a request timeout from caller cancellation using CancellationToken, keeping its inner TimeoutException.
Symptoms
- System.Threading.Tasks.TaskCanceledException: A task was canceled
- The error appears only for slow endpoints
- Retry logic treats a timeout the same as a user cancel
Possible causes
- The HttpClient.Timeout elapsed, which surfaces as TaskCanceledException
- A caller CancellationToken fired, cancelling the request intentionally
- A timeout too small for the endpoint makes normal latency look like a failure
Diagnosis steps
Distinguish timeout from caller cancellation
catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)Interpretation
If the caller token is not cancelled, the exception came from HttpClient.Timeout; the inner exception is a TimeoutException.
Inspect the inner exception
dotnet run 2>&1 | grep -A 3 "TaskCanceledException"Interpretation
A TimeoutException inner exception confirms a timeout; a null inner points to caller cancellation.
Root cause
.NET reports HttpClient timeout and caller cancellation both as TaskCanceledException. Only the CancellationToken state and the inner TimeoutException tell them apart, so code that ignores those conflates deliberate cancellation with a real timeout.
Fix
Use HttpClient.Timeout or a linked CancellationTokenSource, catch TaskCanceledException with a `when (!ct.IsCancellationRequested)` filter to classify timeouts, and propagate genuine cancellation unchanged.
Risk note
Retrying on cancellation wastes work and delays shutdown; retry only classified timeouts and transient errors.
Verify the fix
Simulate a slow endpoint and confirm the log classifies a timeout; cancel via token and confirm it is reported as cancellation, not a retry.
Symptoms
The error “C# HttpClient TaskCanceledException — Timeout Not an Error” points at a specific failure mode. This guide documents the symptoms you can observe, the commands that isolate the cause, the root cause, the fix, and the checks that prove the fix works.
Observed symptoms include:
- System.Threading.Tasks.TaskCanceledException: A task was canceled
- The error appears only for slow endpoints
- Retry logic treats a timeout the same as a user cancel
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Distinguish timeout from caller cancellation
catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
If the caller token is not cancelled, the exception came from HttpClient.Timeout; the inner exception is a TimeoutException.
Step 2: Inspect the inner exception
dotnet run 2>&1 | grep -A 3 "TaskCanceledException"
A TimeoutException inner exception confirms a timeout; a null inner points to caller cancellation.
Likely contributors, in order of frequency:
- The HttpClient.Timeout elapsed, which surfaces as TaskCanceledException
- A caller CancellationToken fired, cancelling the request intentionally
- A timeout too small for the endpoint makes normal latency look like a failure
Root Cause
.NET reports HttpClient timeout and caller cancellation both as TaskCanceledException. Only the CancellationToken state and the inner TimeoutException tell them apart, so code that ignores those conflates deliberate cancellation with a real timeout.
The Fix
Use HttpClient.Timeout or a linked CancellationTokenSource, catch TaskCanceledException with a when (!ct.IsCancellationRequested) filter to classify timeouts, and propagate genuine cancellation unchanged.
Watch out: Retrying on cancellation wastes work and delays shutdown; retry only classified timeouts and transient errors.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
var res = await client.GetAsync(url, cts.Token);
res.EnsureSuccessStatusCode();
}
catch (TaskCanceledException ex) when (!cts.IsCancellationRequested)
{
// timeout: inner exception is a TimeoutException
throw new TimeoutException($"request to {url} timed out", ex);
}
// genuine cancellation flows through unchanged
Verify and Prevent
Simulate a slow endpoint and confirm the log classifies a timeout; cancel via token and confirm it is reported as cancellation, not a retry.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.