Elegantly handle task cancellation
Solution 1
So, what's the problem? Just throw away catch (OperationCanceledException)
block, and set proper continuations:
var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
{
var i = 0;
try
{
while (true)
{
Thread.Sleep(1000);
cts.Token.ThrowIfCancellationRequested();
i++;
if (i > 5)
throw new InvalidOperationException();
}
}
catch
{
Console.WriteLine("i = {0}", i);
throw;
}
}, cts.Token);
task.ContinueWith(t =>
Console.WriteLine("{0} with {1}: {2}",
t.Status,
t.Exception.InnerExceptions[0].GetType(),
t.Exception.InnerExceptions[0].Message
),
TaskContinuationOptions.OnlyOnFaulted);
task.ContinueWith(t =>
Console.WriteLine(t.Status),
TaskContinuationOptions.OnlyOnCanceled);
Console.ReadLine();
cts.Cancel();
Console.ReadLine();
TPL distinguishes cancellation and fault. Hence, cancellation (i.e. throwing OperationCancelledException
within task body) is not a fault.
The main point: do not handle exceptions within task body without re-throwing them.
Solution 2
Here is how you elegantly handle Task cancellation:
Handling "fire-and-forget" Tasks
var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec.
Task.Run( () => {
cts.Token.ThrowIfCancellationRequested();
// do background work
cts.Token.ThrowIfCancellationRequested();
// more work
}, cts.Token ).ContinueWith( task => {
if ( !task.IsCanceled && task.IsFaulted ) // suppress cancel exception
Logger.Log( task.Exception ); // log others
} );
Handling await Task completion / cancellation
var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec.
var taskToCancel = Task.Delay( 10000, cts.Token );
// do work
try { await taskToCancel; } // await cancellation
catch ( OperationCanceledException ) {} // suppress cancel exception, re-throw others
Solution 3
You could do something like this:
public void DoWork(CancellationToken cancelToken)
{
try
{
//do work
cancelToken.ThrowIfCancellationRequested();
//more work
}
catch (OperationCanceledException) when (cancelToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Log.Exception(ex);
throw;
}
}
Solution 4
C# 6.0 has a solution for this..Filtering exception
int denom;
try
{
denom = 0;
int x = 5 / denom;
}
// Catch /0 on all days but Saturday
catch (DivideByZeroException xx) when (DateTime.Now.DayOfWeek != DayOfWeek.Saturday)
{
Console.WriteLine(xx);
}
Eamon
Updated on December 03, 2021Comments
-
Eamon over 2 years
When using tasks for large/long running workloads that I need to be able to cancel I often use a template similar to this for the action the task executes:
public void DoWork(CancellationToken cancelToken) { try { //do work cancelToken.ThrowIfCancellationRequested(); //more work } catch (OperationCanceledException) { throw; } catch (Exception ex) { Log.Exception(ex); throw; } }
The
OperationCanceledException
should not be logged as an error but must not be swallowed if the task is to transition into the cancelled state. Any other exceptions do not need to be dealt with beyond the scope of this method.This always felt a bit clunky, and visual studio by default will break on the throw for
OperationCanceledException
(though I have 'break on User-unhandled' turned off now forOperationCanceledException
because of my use of this pattern).UPDATE: It's 2021 and C#9 gives me the syntax I always wanted:
public void DoWork(CancellationToken cancelToken) { try { //do work cancelToken.ThrowIfCancellationRequested(); //more work } catch (Exception ex) when (ex is not OperationCanceledException) { Log.Exception(ex); throw; } }
Ideally I think I'd like to be able to do something like this:i.e. have some sort of exclusion list applied to the catch but without language support that is not currently possible (@eric-lippert: c# vNext feature :)).public void DoWork(CancellationToken cancelToken) { try { //do work cancelToken.ThrowIfCancellationRequested(); //more work } catch (Exception ex) exclude (OperationCanceledException) { Log.Exception(ex); throw; } }
Another way would be through a continuation:
public void StartWork() { Task.Factory.StartNew(() => DoWork(cancellationSource.Token), cancellationSource.Token) .ContinueWith(t => Log.Exception(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); } public void DoWork(CancellationToken cancelToken) { //do work cancelToken.ThrowIfCancellationRequested(); //more work }
but I don't really like that as the exception technically could have more than a single inner exception and you don't have as much context while logging the exception as you would in the first example (if I was doing more than just logging it).
I understand this is a bit of a question of style, but wondering if anyone has any better suggestions?
Do I just have to stick with example 1?
-
Dennis over 11 yearsThis:
catch (OperationCanceledException) {}
will set the task's status asRanToCompletion
, not asCanceled
. There are use cases, when this is a significant difference. -
Eamon over 11 yearsMy only issue with the continuation method was that I loose context. e.g. If I was processing a list of Items and I needed to log how far through the collection I was when the exception was thrown. I did miss something in my original question which I have fied now and that was a rethrow of the Exception ex to allow the task to transition to the Faulted state.
-
Casey Anderson almost 8 yearsThere are some simpler answers below.
-
Spi almost 7 yearsThe keyword is actually 'when' not 'if'. The syntax (for the OP) would be: catch( Exception ex ) when (!(ex is OperationCanceledException))
-
Ohad Schneider almost 4 yearsI prefer a more strict check:
catch (OperationCanceledException e) when (e.CancellationToken == cancelToken)
-
alv over 2 yearsWith C# 9.0 a nicer syntax: catch (Exception ex) when (ex is not OperationCanceledException)
-
Theodor Zoulias over 2 years@OhadSchneider in general (not in the specific trivial example) there is no guarantee that the
cancelToken
will be propagated through theOperationCanceledException.CancellationToken
. Linked tokens are usually created internally by asynchronous methods, hiding the identity of the originatingCancellationToken
. Checking the conditionwhen (cancelToken.IsCancellationRequested)
is the best you can do in most cases IMHO.