March 19th, 2014

Recommended patterns for CancellationToken

Andrew Arnott
Principal Software Engineer

Whether you’re doing async work or not, accepting a CancellationToken as a parameter to your method is a great pattern for allowing your caller to express lost interest in the result.

Supporting cancelable operations comes with a little bit of extra responsibility on your part.

  1. Know when you’ve passed the point of no cancellation. Don’t cancel if you’ve already incurred side-effects that your method isn’t prepared to revert on the way out that would leave you in an inconsistent state. So if you’ve done some work, and have a lot more to do, and the token is cancelled, you must only cancel when and if you can do so leaving objects in a valid state. This may mean that you have to finish the large amount of work, or undo all your previous work (i.e. revert the side-effects), or find a convenient place that you can stop halfway through but in a valid condition, before then throwing OperationCanceledException. In other words, the caller must be able to recover to a known consistent state after cancelling your work, or realize that cancellation was not responded to and that the caller then must decide whether to accept the work, or revert its successful completion on its own.
  2. Propagate your CancellationToken to all the methods you call that accept one, except after the “point of no cancellation” referred to in the previous point. In fact if your method mostly orchestrates calls to other methods that themselves take CancellationTokens, you may find that you don’t personally have to call CancellationToken.ThrowIfCancellationRequested() at all, since the async methods you’re calling will generally do it for you.
  3. Don’t throw OperationCanceledException after you’ve completed the work, just because the token was signaled. Return a successful result and let the caller decide what to do next. The caller can’t assume you’re cancellable at a given point anyway so they have to be prepared for a successful result even upon cancellation.
  4. Input validation can certainly go ahead of cancellation checks (since that helps highlight bugs in the calling code).
  5. Consider not checking the token at all if your work is very quick, or you propagate it to the methods you call. That said, calling CancellationToken.ThrowIfCancellationRequested() is pretty lightweight so don’t think too hard about this one unless you see it on perf traces.
  6. Check CancellationToken.CanBeCanceled when you can do your work more efficiently if you can assume you’ll never be canceled. CanBeCanceled returns false for CancellationToken.None, and in the future possibly for other cases as well.

Optional CancellationToken parameter

If you want to accept CancellationToken but want to make it optional, you can do so with syntax such as this:

public Task SomethingExpensiveAsync(CancellationToken cancellationToken = default(CancellationToken))
{
  // don't worry about NullReferenceException if the
  // caller omitted the argument because it's a struct.
  cancellationToken.ThrowIfCancellationRequested();
}

Or equivalent in VB:

Function SomethingExpensiveAsync(Optional cancellationToken As CancellationToken = Nothing) As Task
  cancellationToken.ThrowIfCancellationRequested()
End Function

It’s a good idea to only make your CancellationToken parameters optional in your public API (if you have one) and leave them as required parameters everywhere else. This really helps to ensure that you intentionally propagate your CancellationTokens through all the methods you call (#2 above). But of course remember to switch to passing CancellationToken.None once you pass the point of no cancellation.

It’s also a good API pattern to keep your CancellationToken as the last parameter your method accepts. This fits nicely with optional parameters anyway since they have to show up after any required parameters.

Handling cancellation exceptions

If you’ve experienced cancellation before, you’ve probably noticed a couple of types of these exceptions: TaskCanceledException and OperationCanceledException. TaskCanceledException derives from OperationCanceledException. That means when writing your catch blocks that deal with the fallout of a canceled operation, you should catch OperationCanceledException. If you catch TaskCanceledException you may let certain cancellation occurrences slip through your catch blocks (and possibly crash your app).

async Task UserSubmitClickAsync(CancellationToken cancellationToken)
{
  try
  {
    await SendResultAsync(cancellationToken);
  }
  catch (OperationCanceledException ex) // 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 ex)
  {
    // 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;
  }
}

Author

Andrew Arnott
Principal Software Engineer

Principal Software Engineer and OSS contributor. Visual Studio Platform.

7 comments

Discussion is closed. Login to edit/delete existing comments.

  • Ohad Schneider

    Strictly speaking, shouldn’t that be:

    catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken)

    Since theoretically, something could throw an unrelated OperationCanceledException (HttpClient timeout being a prime example).

    • Andrew Arnott

      Yes, that’s a good point. Although not all code that throws OCE will do so in a way to set the CancellationToken property, it may be a good idea to apply the pattern you suggest.

      • Andrew ArnottMicrosoft employee Author

        I’ve updated the blog post with your advice.

  • Rolland Bryan

    Hello Andrew. Do you have any recommendations for the HTTP status code to return when an operation has been cancelled?

    • Andrew Arnott

      I wasn’t even aware that the client could cancel an HTTP request other than to terminate the connection. If there’s a way, then I suppose the HTTP spec would (hopefully) specify what the status code should be. But if there’s no way, then a cancellation exception mid-processing of an HTTP request should be interpreted as a server error in fulfilling the request, and if the client has indeed disconnected, they won’t get that error anyway.

  • Vitaliy K

    Should CancellationToken.ThrowIfCancellationToken() be CancellationToken.ThrowIfCancellationRequested() ?

    • Andrew ArnottMicrosoft employee Author

      Yes, thanks. I’ve corrected it in the post.