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.
- 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.
- 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.
- 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.
- Input validation can certainly go ahead of cancellation checks (since that helps highlight bugs in the calling code).
- 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.ThrowIfCancellationToken() is pretty lightweight so don’t think too hard about this one unless you see it on perf traces.
- 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.
Or equivalent in VB:
Function SomethingExpensiveAsync(Optional cancellationToken As CancellationToken = Nothing) As Task
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)
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)
await httpClient.SendAsync(form, cancellationToken);
// perform your cleanup
// rethrow exception so caller knows you’ve canceled.
// DON’T “throw ex;” because that stomps on
// the Exception.StackTrace property.