Tuesday, October 14, 2025

Cultivated Task Cancellation

Max Seelemann (on his new blog):

So how do you tell if a task supports cancellation? That’s tricky to answer per-se because cancellation is voluntary behavior and needs to be represented in the task’s value or error type. If you’re lucky, cancellation support (or lack thereof) is documented.

[…]

The designated way to check for cancellation is to use static properties like Task.isCancelled. Using static properties may seem odd at first, but it’s clever: APIs called inside the task can check for cancellation without knowing where they’re running or taking task handles as arguments. In fact, such APIs are probably the best way to handle cancellation, as we’ll see shortly.

[…]

You might be thinking, “okay, nice and all. But why go to all this effort to explain something that’s baked into the system anyway?” Well, I fear here comes an inconvenient truth. Not many things in the Standard Library and Foundation have cancellation support built in. In fact, as far as I know, it’s just three.

Update (2025-10-15): Max Seelemann:

  • The Task initializer is declared as @discardableResult, which means you can create a task and let go of its handle without a warning, like Task {…}. This makes it too easy to unintentionally set up a task that’s forgotten and runs forever.
  • It’s too easy to forget to call the cancel method. Especially when the task is stored in an array or dictionary rather than a single instance variable. Every task handle would need to be cancelled individually when removed or replaced.

[…]

To solve number 2, we wrote a little wrapper called ScopedTask, which handles cancellation automatically.

[…]

I published the source for ScopedTask on GitHub—feel free to use or adapt it to your needs.

Update (2025-10-21): Max Seelemann:

The signature of the cancellation handler is @Sendable () -> Void—a synchronous sendable non-throwing non-isolated closure with no arguments and no return value. Which means:

  • You cannot “return” or “throw” out of the main operation from the handler; you can only influence its course of execution.
  • To achieve that, you must use only sendable (thread-safe) constructs.
  • And these constructs must be synchronously accessible.

[…]

The only standard library construct that fulfills all these requirements out of the box is Task itself. We’ll see an example shortly. But for anything else, you have no choice but to go to lower-level mechanisms…like locks.

[…]

So that is a simple function that suspends until cancelled. 😅 Only that, in my opinion, it’s not simple at all—it’s really complex with numerous pitfalls small and large. Should this keep you from implementing cancellable tasks? It shouldn’t, but you should sharpen your senses.

Comments RSS · Twitter · Mastodon

Leave a Comment