Fix: C# TaskCanceledException: A task was canceled
Quick Answer
How to fix C# TaskCanceledException A task was canceled caused by HttpClient timeouts, CancellationToken, request cancellation, and Task.WhenAll failures.
The Error
Your C# application throws:
System.Threading.Tasks.TaskCanceledException: A task was canceled.Or variations:
System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.System.OperationCanceledException: The operation was canceled.System.Net.Http.HttpRequestException: The request was canceled due to the configured HttpClient.Timeout
---> System.Threading.Tasks.TaskCanceledExceptionTaskCanceledException: A task was canceled.
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)An asynchronous operation was canceled before it completed. This usually means an HTTP request timed out, a CancellationToken was triggered, or a task was explicitly canceled.
Why This Happens
In .NET, TaskCanceledException is thrown when:
- An HTTP request exceeds
HttpClient.Timeout. The default timeout is 100 seconds. - A
CancellationTokenis canceled. Code explicitly requested cancellation. - The request is aborted. In ASP.NET, the client disconnected before the response was sent.
Task.WhenAllpropagates cancellation. One task cancels, affecting others.- Deadlocks in async code. Blocking on async code with
.Resultor.Wait()can cause timeouts.
The most common cause in production applications is HTTP request timeouts from HttpClient.
Fix 1: Increase HttpClient Timeout
The default HttpClient.Timeout is 100 seconds. For slow APIs or large uploads:
Broken:
var client = new HttpClient();
var response = await client.GetAsync("https://slow-api.example.com/large-report");
// TaskCanceledException after 100 secondsFixed — increase timeout:
var client = new HttpClient
{
Timeout = TimeSpan.FromMinutes(5)
};
var response = await client.GetAsync("https://slow-api.example.com/large-report");Fixed — per-request timeout using CancellationToken:
var client = new HttpClient
{
Timeout = Timeout.InfiniteTimeSpan // Disable global timeout
};
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var response = await client.GetAsync("https://slow-api.example.com/large-report", cts.Token);With IHttpClientFactory (recommended):
// In Program.cs or Startup.cs
builder.Services.AddHttpClient("SlowApi", client =>
{
client.BaseAddress = new Uri("https://slow-api.example.com/");
client.Timeout = TimeSpan.FromMinutes(5);
});
// In your service
public class MyService
{
private readonly HttpClient _client;
public MyService(IHttpClientFactory factory)
{
_client = factory.CreateClient("SlowApi");
}
}Pro Tip: Do not create
new HttpClient()for every request. This causes socket exhaustion. UseIHttpClientFactoryor a singlestatic HttpClientinstance. If you use a static instance, set the timeout once during initialization.
Fix 2: Handle Timeouts Gracefully
Distinguish between timeouts and explicit cancellations:
try
{
var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient timeout
_logger.LogWarning("Request to {Url} timed out", url);
throw new TimeoutException($"Request to {url} timed out", ex);
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Explicit cancellation (user or application shutdown)
_logger.LogInformation("Request to {Url} was canceled", url);
throw;
}
catch (TaskCanceledException ex)
{
// Other cancellation
_logger.LogError(ex, "Request to {Url} was unexpectedly canceled", url);
throw;
}In .NET 6+, the distinction is clearer:
try
{
var response = await client.GetAsync(url, cancellationToken);
}
catch (TaskCanceledException ex) when (ex.CancellationToken != cancellationToken)
{
// Timeout — the HttpClient's internal token was canceled, not ours
Console.WriteLine("Request timed out");
}
catch (TaskCanceledException)
{
// Our token was canceled
Console.WriteLine("Request was explicitly canceled");
}Fix 3: Fix CancellationToken Usage
Passing a CancellationToken that gets canceled too early:
Broken — token cancels immediately:
using var cts = new CancellationTokenSource();
cts.Cancel(); // Canceled before the request even starts!
var response = await client.GetAsync(url, cts.Token);Broken — timeout too short:
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
var response = await client.GetAsync(url, cts.Token); // 100ms is too short for most HTTP requestsFixed — reasonable timeout:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await client.GetAsync(url, cts.Token);Linking cancellation tokens:
// Combine a user cancellation token with a timeout
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, // From the caller
timeoutCts.Token // Our timeout
);
var response = await client.GetAsync(url, linkedCts.Token);Common Mistake: Not disposing
CancellationTokenSource. Always useusingor call.Dispose(). UndisposedCancellationTokenSourceobjects with timeouts leak timer resources.
Fix 4: Fix ASP.NET Request Cancellation
In ASP.NET, HttpContext.RequestAborted is triggered when the client disconnects:
[HttpGet("data")]
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
// cancellationToken is automatically bound to HttpContext.RequestAborted
try
{
var data = await _service.GetDataAsync(cancellationToken);
return Ok(data);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Client disconnected — this is normal, not an error
_logger.LogDebug("Client disconnected during GetData");
return StatusCode(499); // Client Closed Request (non-standard)
}
}Pass the token through your entire call chain:
public async Task<Data> GetDataAsync(CancellationToken ct)
{
var dbResult = await _dbContext.Items
.Where(i => i.Active)
.ToListAsync(ct); // Pass token to EF Core
var apiResult = await _httpClient
.GetAsync("/api/external", ct); // Pass token to HttpClient
return new Data(dbResult, apiResult);
}Fix 5: Fix Retry Logic with Polly
Use Polly for structured retry and timeout policies:
using Polly;
using Polly.Extensions.Http;
// In Program.cs
builder.Services.AddHttpClient("MyApi")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetTimeoutPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>() // Retry on timeout
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy<HttpResponseMessage> GetTimeoutPolicy()
{
return Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
}Simple retry without Polly:
async Task<HttpResponseMessage> GetWithRetry(string url, int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
return await _client.GetAsync(url);
}
catch (TaskCanceledException) when (i < maxRetries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
throw new TimeoutException($"Request to {url} failed after {maxRetries} retries");
}Fix 6: Fix Async Deadlocks
Blocking on async code causes timeouts that appear as TaskCanceledException:
Broken — deadlock in synchronous context:
// In a non-async method (e.g., MVC action filter, constructor)
var result = GetDataAsync().Result; // DEADLOCK! Blocks the threadFixed — use async all the way:
// Make the calling method async
var result = await GetDataAsync();If you must call async from sync (rare):
// Use Task.Run to avoid capturing the synchronization context
var result = Task.Run(() => GetDataAsync()).GetAwaiter().GetResult();Fixed — use .ConfigureAwait(false) in library code:
public async Task<string> GetDataAsync()
{
var response = await _client.GetAsync(url).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}Fix 7: Fix Task.WhenAll Cancellation
When one task in Task.WhenAll fails, the others may be canceled:
var tasks = new[]
{
client.GetAsync("https://api1.example.com/data"),
client.GetAsync("https://api2.example.com/data"),
client.GetAsync("https://api3.example.com/data"),
};
try
{
var results = await Task.WhenAll(tasks);
}
catch (TaskCanceledException)
{
// Which task was canceled?
foreach (var task in tasks)
{
if (task.IsCanceled)
Console.WriteLine("Task was canceled");
else if (task.IsFaulted)
Console.WriteLine($"Task faulted: {task.Exception?.InnerException?.Message}");
else
Console.WriteLine("Task completed successfully");
}
}Run tasks independently with error handling:
var results = await Task.WhenAll(
SafeGet(client, "https://api1.example.com/data"),
SafeGet(client, "https://api2.example.com/data"),
SafeGet(client, "https://api3.example.com/data")
);
async Task<string?> SafeGet(HttpClient client, string url)
{
try
{
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
catch (TaskCanceledException)
{
return null; // Return null instead of throwing
}
}Fix 8: Fix Background Service Cancellation
In hosted services, StoppingToken is canceled during shutdown:
public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Application is shutting down — this is expected
_logger.LogInformation("Service is stopping");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background service");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
}Still Not Working?
Check for DNS resolution timeouts. Slow DNS can cause the connection phase to timeout before the HTTP request even starts:
var handler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(5),
};
var client = new HttpClient(handler);Check for proxy issues. A corporate proxy might cause connection delays.
Enable HttpClient logging:
builder.Services.AddHttpClient("MyApi")
.ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(30); })
.AddHttpMessageHandler<LoggingHandler>();Check for thread pool starvation. If the thread pool is exhausted, tasks wait indefinitely:
ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
Console.WriteLine($"Available workers: {workerThreads}, IO: {completionPortThreads}");For C# null reference exceptions, see Fix: C# System.NullReferenceException. For type conversion errors, see Fix: C# Cannot implicitly convert type.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: C# async deadlock — Task.Result and .Wait() hanging forever
How to fix the C# async/await deadlock caused by Task.Result and .Wait() blocking the synchronization context in ASP.NET, WPF, WinForms, and library code.
Fix: C# System.NullReferenceException: Object reference not set to an instance of an object
How to fix C# NullReferenceException caused by uninitialized objects, null returns, LINQ results, async/await, dependency injection, and Entity Framework navigation properties.
Fix: C# Cannot implicitly convert type 'X' to 'Y'
How to fix C# cannot implicitly convert type error caused by type mismatches, nullable types, async return values, LINQ result types, and generic constraints.
Fix: Angular ExpressionChangedAfterItHasBeenCheckedError
How to fix ExpressionChangedAfterItHasBeenCheckedError in Angular caused by change detection timing issues, lifecycle hooks, async pipes, and parent-child data flow.