48

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 for OperationCanceledException 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:
public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (Exception ex) exclude (OperationCanceledException)
    {
        Log.Exception(ex);
        throw;
    }
}
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 :)).

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?

Eamon
  • 1,829
  • 1
  • 20
  • 21
  • I use a solution where the logging framework do different things for some exceptions. i.e. ignore OperationCancelledException, flattens AggregateException, logs only innerException for InvalidOperationException etc – adrianm Sep 28 '12 at 06:42

6 Answers6

21

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.

Dima
  • 1,717
  • 15
  • 34
Dennis
  • 37,026
  • 10
  • 82
  • 150
  • 1
    My 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. – Eamon Sep 28 '12 at 06:35
  • There are some simpler answers below. – Casey Anderson May 05 '16 at 15:15
15

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
Casey Anderson
  • 313
  • 3
  • 12
11

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;
    }
}
Jesper Meyer
  • 111
  • 1
  • 4
  • 1
    I prefer a more strict check: `catch (OperationCanceledException e) when (e.CancellationToken == cancelToken)` – Ohad Schneider Jul 13 '20 at 14:54
  • 2
    @OhadSchneider in general (not in the specific trivial example) there is no guarantee that the `cancelToken` will be propagated through the `OperationCanceledException.CancellationToken`. Linked tokens are usually created internally by asynchronous methods, hiding the identity of the originating `CancellationToken`. Checking the condition `when (cancelToken.IsCancellationRequested)` is the best you can do in most cases IMHO. – Theodor Zoulias Nov 21 '21 at 14:36
10

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);
}
Lennart
  • 9,657
  • 16
  • 68
  • 84
user5628548
  • 101
  • 1
  • 2
  • 16
    The keyword is actually 'when' not 'if'. The syntax (for the OP) would be: catch( Exception ex ) when (!(ex is OperationCanceledException)) – Spi Jul 18 '17 at 14:21
  • With C# 9.0 a nicer syntax: catch (Exception ex) when (ex is not OperationCanceledException) – alv Sep 02 '21 at 05:29
0

According to this MSDN blog post, you should catch OperationCanceledException, e.g.

async Task UserSubmitClickAsync(CancellationToken cancellationToken)
{
   try
   {
      await SendResultAsync(cancellationToken);
   }
   catch (OperationCanceledException) // includes TaskCanceledException
   {
      MessageBox.Show(“Your submission was canceled.”);
   }
}

If your cancelable method is in between other cancelable operations, you may need to perform clean up when canceled. When doing so, you can use the above catch block, but be sure to rethrow properly:

async Task SendResultAsync(CancellationToken cancellationToken)
{
   try
   {
      await httpClient.SendAsync(form, cancellationToken);
   }
   catch (OperationCanceledException)
   {
      // perform your cleanup
      form.Dispose();

      // rethrow exception so caller knows you’ve canceled.
      // DON’T “throw ex;” because that stomps on 
      // the Exception.StackTrace property.
      throw; 
   }
}
RB.
  • 36,301
  • 12
  • 91
  • 131
Bondolin
  • 2,793
  • 7
  • 34
  • 62
-3

I am not entirely sure of what you are trying to achieve here but I think the following pattern might help

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException) {}
    catch (Exception ex)
    {
        Log.Exception(ex);
    }
}

You might have observed that I have removed the throw statement from here. This will not throw the exception but will simply ignore it.

Let me know if you intend to do something else.

There is yet another way which is quite close to what you have exhibited in your code

    catch (Exception ex)
    {
        if (!ex.GetType().Equals(<Type of Exception you don't want to raise>)
        {
            Log.Exception(ex);

        }
    }
Murtuza Kabul
  • 6,438
  • 6
  • 27
  • 34
  • 9
    This: `catch (OperationCanceledException) {}` will set the task's status as `RanToCompletion`, not as `Canceled`. There are use cases, when this is a significant difference. – Dennis Sep 28 '12 at 05:44