System.Threading.Tasks.TaskCanceledException

A TaskCanceledException is thrown when you await a task that was canceled. It derives from OperationCanceledException, and the two are often mixed up in catch blocks.

The most common source is HttpClient. A request that takes longer than the Timeout of the client is canceled, and the message says The request was canceled due to the configured HttpClient.Timeout. It also happens when a CancellationToken is canceled on purpose, for example when a user closes the browser tab.

Minimum version: >= 4.0 >= Core 1.0

Statistics

30
elmah.io logo 9

Common causes

A cancellation is either a timeout or a token that was canceled. The first thing to find out is which one it was.

An HttpClient request that times out

The default timeout of an HttpClient is 100 seconds. If the server is slow, or the network is down and the connection hangs, the request is canceled when the time runs out. The exception does not say that it was a timeout in older versions of .NET, where you only get A task was canceled.

var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var json = await client.GetStringAsync("https://api.example.com/prices");   // TaskCanceledException if no response in 30 seconds

Find out why the server is slow first. If the call is legitimately slow, give it more time, per request, with a cancellation token:

var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
var json = await client.GetStringAsync("https://api.example.com/export", cts.Token);

If a token is passed down to the task and the source is canceled, or CancelAfter time runs out, the task throws when it reaches the next place that checks the token.

using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
await Task.Delay(TimeSpan.FromSeconds(5), cts.Token);   // TaskCanceledException

Make sure the time you allow is long enough for the work, and treat a cancellation you asked for as a normal result:

using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));

try
{
    await Task.Delay(TimeSpan.FromSeconds(5), cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Stopped after the time limit.");
}

In ASP.NET Core, HttpContext.RequestAborted is canceled when the browser or the caller disconnects. If you pass it on to database and HTTP calls, they throw when the caller leaves. This is expected, and not a problem in your code.

Do not log it as an error. Let the exception end the request, or handle OperationCanceledException with a filter on the token.

How to fix it and prevent it

Tell a timeout from a cancellation

Check whether your own token was canceled. If it was not, the cancellation came from somewhere else, most likely a timeout.

public async Task<string> GetPrices(HttpClient client, CancellationToken cancellationToken)
{
    try
    {
        return await client.GetStringAsync("https://api.example.com/prices", cancellationToken);
    }
    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
    {
        throw;   // the caller canceled, not an error
    }
    catch (TaskCanceledException e)
    {
        throw new TimeoutException("The price service did not answer in time.", e);
    }
}

For calls to services you do not control, use the resilience handler from Microsoft.Extensions.Http.Resilience. It adds a timeout per attempt, a total timeout and retries, so a slow response does not have to end up as an exception.

builder.Services.AddHttpClient("prices", client => client.BaseAddress = new Uri("https://api.example.com"))
    .AddStandardResilienceHandler();

Pass the CancellationToken to every async call that accepts one, so the work stops when it is no longer needed. Create the HttpClient once, or with IHttpClientFactory, to avoid running out of sockets that make requests slow.

How to read the stack trace

A timeout from HttpClient has an inner TimeoutException, and the message mentions the timeout:

System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing.
 ---> System.TimeoutException: A task was canceled.
 ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.SocketsHttpHandler.<SendAsync>g__CreateHandlerAndSendAsync|115_0(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpClient.HandleFailure(Exception e, Boolean telemetryStarted, HttpResponseMessage response, CancellationTokenSource cts, CancellationToken cancellationToken, CancellationTokenSource pendingRequestsCts)
   at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at Shop.Services.PriceClient.GetPrices(String url) in C:\src\Shop\Services\PriceClient.cs:line 12
   at Program.<Main>$(String[] args) in C:\src\Shop\Program.cs:line 5

The message says that the timeout of 30 seconds was reached, and the inner TimeoutException confirms it. The frame PriceClient.GetPrices on line 12 of PriceClient.cs is the call that waited. A cancellation from a token has no inner TimeoutException. Check what the service did during those seconds, and not only the client.

Should you catch it?

Catch it when you can do something about a slow dependency, such as retrying or returning a cached value. Catch OperationCanceledException to also include the plain cancellation, because TaskCanceledException derives from it.

Do not swallow it everywhere. A canceled token should stop the work, so rethrow when your own token was canceled.

Find TaskCanceledException before your users do

elmah.io logs every unhandled exception in your .NET application with its stack trace and request details, groups identical errors and notifies you when a new one appears.

Start free trial
Free 21-day trial No credit card required

Related exceptions

Frequently asked questions

What is the difference between TaskCanceledException and OperationCanceledException?

TaskCanceledException derives from OperationCanceledException. It is thrown when a Task was canceled, while the base type is thrown by code that checks a token. Catch the base type to handle both.

In current versions of .NET, the message says configured HttpClient.Timeout and the inner exception is a TimeoutException. Otherwise, check whether your own token is canceled. If it is not, assume a timeout.

Only after you know why the call is slow. A longer timeout keeps threads and connections busy for longer, and a slow server may still not answer. A retry with a short timeout often works better.

The request token is canceled when the caller disconnects, and the calls that use it throw. Filter these out of your error logging.

Let your AI agent track it down

Connect Claude Code, Cursor, VS Code or Visual Studio to the elmah.io MCP server and ask your agent to look into TaskCanceledException for you. For example:

AI chat
Find the most frequent TaskCanceledException in my production log and show me the line that throws it.

The agent reads the stack trace and request details from elmah.io, finds the code in your repository and proposes a fix. In Claude Code, add the server with one command:

claude mcp add --transport http --client-id claudecode elmahio https://mcp.elmah.io/mcp

The MCP server is included on every plan and is currently in beta. Set up the MCP server.

Further reading

YouTube videos

Answers from Stack Overflow

If you add ContinueWith() with an empty action, the exception isn't thrown. The exception is caught and passed to the tsk.Exception property in the ContinueWith(). But It saves you from writing a try/catch that uglifies your code.

await Task.Delay(500, cancellationToken.Token).ContinueWith(tsk => { });

UPDATE:

Instead of writing code to handle the exception, a boolean would be much cleaner. This is only preferred when a delay cancel is expected!. One way is to create a helper class (Although I don't like helper classes much)

namespace System.Threading.Tasks
{
    public static class TaskDelay
    {
        public static Task<bool> Wait(TimeSpan timeout, CancellationToken token) =>
            Task.Delay(timeout, token).ContinueWith(tsk => tsk.Exception == default);

        public static Task<bool> Wait(int timeoutMs, CancellationToken token) =>
            Task.Delay(timeoutMs, token).ContinueWith(tsk => tsk.Exception == default);
    }
}

For example:

var source = new CancellationTokenSource();

if(!await TaskDelay.Wait(2000, source.Token))
{
    // The Delay task was canceled.
}

(don't forget to dispose the source)

By Jeroen van Langen. Read the original answer on Stack Overflow.

The difference here comes from using token.ThrowIfCancellationRequested(). This method checks for cancellation and if requested throws OperationCanceledException specifically and not TaskCanceledException (understandable as CancellationToken isn't exclusive to the TPL). You can look at the reference source and see that it calls this method:

private void ThrowOperationCanceledException()
{
    throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this);
}

"Regular" cancellation though will indeed generate a TaskCanceledException. You can see that by cancelling the token before the task had a chance to start running:

cancellationTokenSource.Cancel();
var task = Task.Run(() => { }, cancellationTokenSource.Token);
try
{
    await task; 
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
    Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}");
    Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}");
    Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}");
}

Output:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Sandbox.Program.<MainAsync>d__1.MoveNext()
Task.IsCanceled: True
Task.IsFaulted: False
Task.Exception: null

Traditional .Net methods usually don't use CancellationToken.ThrowIfCancellationRequested for async API as this is only appropriate when offloading work to another thread. These methods are for inherently asynchronous operations so cancellation is monitored using CancellationToken.Register (or the internal InternalRegisterWithoutEC).

By i3arnon. Read the original answer on Stack Overflow.

TaskCanceledException inherits from OperationCanceledException. So it least there is a little consitency.

if( ex is OperationCanceledException)
{
...
}

By Tomas Kubes. Read the original answer on Stack Overflow.

Your problem is that SmtpClient is disposed before the email is sent.

Two ways:

  • Await the SendMailAsync result

    using (var client = new SmtpClient())
    {
        await client.SendMailAsync(email);
    }
    
  • Register the SendCompleted event and dispose the SmtpClient only after the message is sent

    var client = new SmtpClient();
    client.SendCompleted += (s, e) => {
        client.Dispose();
    };
    return client.SendMailAsync(message);
    

By meziantou. Read the original answer on Stack Overflow.

That's to be expected. When you cancel the old Delay, it will raise an exception; that's how cancellation works. You can put a simple try/catch around the Delay to catch the expected exception.

Note that if you want to do time-based logic like this, Rx is a more natural fit than async.

By Stephen Cleary. Read the original answer on Stack Overflow.