SE-0504: Task Cancellation Shields

Hello, Swift community.

The review of SE-0504: Task Cancellation Shields begins now and runs through January 26th, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via either email or forums DM. When messaging me directly, please put "[SE-0504]" at the start of the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available on the evolution website.

Thank you,

John McCall
Review Manager

18 Likes

I’ve read the proposal but haven’t had a lot of time to think about it, yet.

I’ve previously just spawned unstructured tasks for cleanup. It’s not that big a hassle. Generally, the biggest hassle is realising that you need some kind of cleanup protection in the first place – a problem that isn’t addressed by this proposal.

I suppose this proposal is much better if you’re cleaning up a bunch a non-sendable data or you otherwise can’t transition to a new Task for some reason. But having a Task that is cancelled but behaves like it is not cancelled is weird.

My mind is filled with questions like: what happens if I try to cancel the Task from inside the shield? If I create a new child task will it be able to self-cancel?

2 Likes

I am strongly in favor of solving this problem and the solution proposed here. I have written a lot of asynchronous code that manages resources which heavily relies on clean-up that must run even in cancelled tasks. While the existing workaround with using an unstructured task is applicable in some situations there are many where it can't be used.

public func withTaskCancellationShield<Value, Failure>(
  _ operation: () throws(Failure) -> Value,
  file: String = #fileID, line: Int = #line
) throws(Failure) -> Value

public nonisolated(nonsending) func withTaskCancellationShield<Value, Failure>(
  _ operation: nonisolated(nonsending) () async throws(Failure) -> Value,
  file: String = #fileID, line: Int = #line
) async throws(Failure) -> T

We should allow ~Copyable types for Value here.

2 Likes

We’re basically saying this isn’t viable in situations where you just can’t spare the scheduling delay and indeterminism of such cleanups with extra tasks. If you’re getting away with it that’s fine, but we’ve observed production use-cases which suffer because of this indeterminism.

There’s various aspects you could be asking about here, this is documented in the “debugging and observing task cancellation shields” section, please give it another read and happy to answer more specific questions if you’ll still have any.

The proposal spells all this out as well, shields don’t affect child tasks directly. Just the propagation from parent cancellation. If you have concrete examples you’d like to discuss please spell them out, it’s hard to answer vague questions on this topic.

1 Like

Yeah, that’s a fair point – we can add that to all the generic signatures.

4 Likes

Great feature!
May I suggest an alternative name, though? If I'm honest, withTaskCancellationShield had me guessing about the meaning until I read the proposal's introduction.

My suggestion is withTaskCancellationIgnored.

It echoes existing APIs like ObservationIgnored and AnimatableIgnored, granted these are library Macro naming conventions, but I think it works here too. The word "ignore" is also used in the very first line of the proposal intro, so it seems like the authors are very much in the same headspace.

7 Likes

We explicitly rejected the word “ignore” (alternatives consideed). It is problematic because it implies the cancellation had no effect but that’s not the case, you’re just not observing it while executing inside a shielded block of code. “ignoring” would imply the task was not getting cancelled if cancellation happened while in such a block, so that’s a risky misunderstanding we want to avoid, and therefore are proposing coining a new term, because the exact behavior is not exactly captured by any other verb we could think of.

In other words, I think that leading people to intuitively mis-understand a feature, rather than not having to look up its semantics when they encounter it would be worse.

5 Likes

“ignoring” would imply the task was not getting cancelled if cancellation happened while in such a block

I see, fair enough. Just to double check, these are the two scenarios you're talking about, right?

// Scenario 1 - with Unsafe Task

Task.isCancelled // false
withTaskCancellationIgnored { // a misleading name
  withUnsafeCurrentTask { $0?.cancel() } // not ignored!
  Task.isCancelled // false 
}
Task.isCancelled // true


// Scenario 2 - captured Task
var task: Task<Void, Never>?
task = Task {
  Task.isCancelled // false
  withTaskCancellationIgnored { // a misleading name
    task?.cancel() // not ignored!
    Task.isCancelled // false 
  }
  Task.isCancelled // true
}
3 Likes

That’s two examples, yes. That’s why just “ignoring cancellation” isn’t quite right, we didn’t ignore the cancel() happening after all.

There’s many more cases, just any “someone cancels the task, while the task is executing with a cancellation shield installed” manifests in the same behavior.

3 Likes

I continue to think that this is an important enough point—the distinction between "the effect of being canceled" and "being canceled"—that consistent feedback wondering why it's not called "ignored" sends a signal that using "shield" as a term of art is not a good enough solution.

I think in part it is because the analogy is faulty: as we discussed during the pitch phase, if you shoot an arrow at a shield, the arrow doesn't hover in midair waiting to hit the target once the shield is dropped. By the "shield" analogy, an arrow blocked by a shield is ignored, so substituting one term for the other doesn't solve the problem.

This is a niche enough feature with a significant enough nuance that—based on this empiric evidence of ongoing, continued reader confusion—I would urge continued exploration as to what terminology could make the model clearer. Other terms that come into mind which do convey the sense that something is temporarily held back rather than overridden:

  • withTaskCancellationDeferred — I think this conveys the right idea by analogy with defer that it's a "clean-up" feature that wraps things up; in this sense and by analogy, it head-on contradicts the misunderstanding that something is "ignored" forever
  • withTaskCancellationSuspended
  • withTaskCancellationInAbeyance — (now we're lawyers)

I continue to think that, even only on UnsafeCurrentTask, we should not have this spelled out as an API. I guess some of this is contingent on whether the same result can already be achieved by querying task.isCancelled == Task.isCancelled, based on the next section.

Regardless, though, this situation rhymes with prior instances where we've said that adding some API but then obfuscating it with namespaces or in wordier types is not the way: either it's good API or it's not. Namespacing it under UnsafeCurrentTask might be justifiable if the API is literally concurrency-unsafe, but afaict it's not (at least not per se)—the motivation given, at least, is to just hide it or make it seem scarier so it's less used.


I think this is the correct behavior. However, the proposal is ambiguous on a salient point. It says only that the instance method task.isCancelled queried from outside of the task will return the actual cancelled state regardless of cancellation shield. The rationale given is that it is racy to consider any other behavior.

However, what is the proposed behavior if the instance method is queried from inside of the task? It would not be racy: by that logic, it would seem that it should respect cancellation shields: in other words, Task.isCancelled == task.isCancelled always if task is the current task. However, there is also a case to be made that we should have task.isCancelled always behave as-though it is not in a cancellation shield scope. This would make the API exhibit the same behavior in every context—much simpler to understand.

(It would also incidentally make Task.isCancelled == task.isCancelled a way to query if we're under a shield for debugging purposes, without vending a dedicated API that we have to then warn people not to use in production—see previous section.)

Either way, this proposal should spell out explicitly what the behavior is of task.isCancelled when it is invoked in the context where task is the current task.

10 Likes

This topic is pretty far outside of my wheelhouse, and I am very much not an expert in matters of concurrency. However, from reading the discussions about naming this API, I wonder if it might be cleaner to switch from a mental model of:

(a) “Perform these actions while pretending the task is not cancelled.”

To something more like:

(b) “Perform these actions even if the task is cancelled.”

That line of thought could perhaps lead to a new set of spellings that are potentially easier to understand.

7 Likes

I believe the issue with such a formulation is that any code that is called down the stack might check Task.isCancelled without the appropriate “even if the task is cancelled” protection. That is, there would be no way for the caller of library code to influence the observed cancellation state. We already have a mechanism for “perform these actions even if the task is cancelled” which is simply “don’t check the cancellation state.”

I am talking entirely and exclusively about the mental model, and not at all about the implementation mechanism.

As in, keep the exact same implementation, and have it behave in the exact same way as in the proposal, but think about it differently.

Then name the API to match the new way of thinking about it.

I admit i have only skimmed the discussion here, so if this has been brought up before feel free to ignore me :blush:

The explanations for the precise behaviour seem to differentiate cancellation from the outside and from inside the task. Maybe it would be worth including that in the name for more clarity? Something like ignoringOutsideCancellation or ignoringParentCancellation perhaps?

1 Like

withoutPropagatingTaskCancellation is a bit of a mouthful, but is less vague than "shield" in my eyes. That being said, we've been calling them "shields" for a while now.

2 Likes

I actually kind of feel the opposite—in particular, this feedback:

is IMO exactly what I think a 'new term of art' should aim to achieve. The semantics here are very subtle and I don't think we're going to be able to pick a name which sufficiently communicates in three or four words how all the different edge cases will behave. It's much better for people who encounter this to scratch their heads a bit before they read the docs than to read a succinct name which implies something incorrect about the behavior.

I still think that shield is fairly evocative for the behavior described here, if one adopts the mindset that it's the scope that is shielded rather than a notional observer who moves along with the program counter. Moreover, task cancellation does not act like an arrow, but more like sunlight: cancellation hits the entire task 'at once', but the notional observer may ride the program counter between shielded regions where they can be shaded and pretend its still night.

In any case, I agree that our docs for this feature should take great pains to explain why this behavior is different from 'ignoring' cancellation, but I don't think it's the job of the name to do much more than think "huh, this has some effect on task cancellation... what does it do again?" And I am perfectly happy if we have minor qualms about how perfect the metaphor is so long as it doesn't easily lead to misinterpretations. Judging alternatives from this thread by that standard:

  • withTaskCancellationDeferred: I think this suggests something too similar to 'ignored', that cancellations which happen inside the block would not actually have any effect until the end. But IIUC if there were a task cancellation handler outside the shield then it would run immediately upon a call to task.cancel from within the shield.
  • withTaskCancellationSuspended: "suspended" as a term of art here is decent, IMO. I don't think this immediately suggests any improper semantics.
  • withTaskCancellationInAbeyance: doesn't suggest anything wrong, but IMO is too obscure—assume this one was half-joking. :slightly_smiling_face:
  • ignoringOutsideCancellation: decent qualification of the "ignore" terminology IMO! It misses the with and the Task as part of the name, and withOutsideTaskCancellationIgnored starts to get pretty wordy IMO. "Outside" is not immediately clear, so clears the bar of "gets someone to look at the docs".
  • ignoringParentCancellation: the problem with this that I see is that this is really an operation that affects the execution of the current task rather than the parent task—we don't really talk about the current execution environment having the current task as a 'parent'.
  • withoutPropagatingTaskCancellation: I think this too easily suggests only the "child tasks won't get cancelled if parent task gets cancelled without hinting at all that there's something more (i.e., Task.isCancelled starts lying.

Agree that this should be the behavior if it isn't already, and that the proposal should make this clear!

While I don't see an example in the proposal that confirms this, I assumed that hasActiveTaskCancellationShield would return true even if the task is not cancelled—that is, in:

Task {
  withTaskCancellationShield {
    withUnsafeCurrentTask {
      print($0!.hasActiveTaskCancellationShield)
    }
  }
}

we'd always expect it to print true. Granted, given that the stated use case is to demystify "why isn't my Task.isCancelled true?" I don't know that the above behavior is needed. But also, giving an API for this specific scenario is IMO more useful than relying on the impicit knowledge that Task.isCancelled != task.isCancelled always means "you're inside a task cancellation shield". I think it's reasonable to give an UnsafeCurrentTask property for inspecting the value of this task record property directly rather than merely having a roundabout way of spelling it.

3 Likes

Other terms that come into mind which do convey the sense that something is temporarily held back rather than overridden:

I think the temporary nature of the applied effect is already conveyed well by the with-style API, and reinforcing this with additional verbiage isn't necessary.

For this reason, and after reading other arguments, I'm now more open to withTaskCancellationShield naming.

I think for a with-style API it's better to focus on what IS in effect, rather than on what IS NOT.

In this case, the API conveys that Cancellation Shield is in effect for a given scope. What is a Cancellation Shield? You can learn about it in the documentation. This is in contrast to an API that tries to convey what is not in effect, which is cancellation propagation via static Task methods (but not the task-instance ones).

4 Likes

This is definitely a very good addition and personally, I am strongly in favor of adopting the proposal mostly as is, including the "cancellation shield" term of art. I want to point out that IIRC, this has been used to describe the purpose of the "use an unstructured task" pattern before.

While it's true that in the real world, a shield "permanently" blocks an arrow (including from progressing "further down the tree"), I'd argue that this is just another example of such metaphors breaking down at a certain point in computer science anyway.[1]
In the end, the cancellation shield "shields" only the code that's in the scope of the closure that's passed to withTaskCancellationShield, and that code is indeed "permanently" shielded (excluding deeper scopes, as the proposal explains).
With the term as is, I see the best fit for progressive disclosure, "cancellation shield" is hopefully unexpected enough for developers new to the concept to realize "wait, why is it not just named ignored or so? This is a new concept I need to learn?"

What I'd also like to see added is the clarification for the isCancelled instance method, as @xwu pointed out. This could be addressed by clarifying (in the documentation for the cancellation shields) that this method may be called from outside the task (and usually is), so it is, unlike its static counterpart, not affected by the shield in general (in effect excluding it from anything shield related, correct?).
I'm a little on the edge of the hasActiveTaskCancellationShield and want to point out that regardless of any "this is intended for debugging only" warnings in the documentation, it will be used in production. However, if there are no huge dangers (e.g. performance considerations), I'd be fine with this. Just keep in mind that this will lead to discussions (read: probably heated arguments) down the line, similar to Thread.current. :sweat_smile:


  1. I still remember people who learned that "software" is called "soft" because the 5.25 inch floppy disks it comes on are, well, "soft and floppy"... :sweat_smile: (and the "hardware" was a "hard metal box"... don't grill me on this, I'm saying this tongue in cheek). ↩︎

2 Likes

Big +1 from my side.

I’ll try to reply to bigger topics coming up througout the thread.

That’s not really right, because the problem this proposal is solving isn’t code you control – it is code you don’t control.

It is trivial to not check cancellation in your own code, it is impossible to do that in already existing 3rd party code that you cannot change – e.g. a networking library which will avoid making a request from cancelled context. This is why we need to give the caller of such API the ability to wrap a piece of code with a shield or however we’d like to name it.

Yeah, I very much agree with that… I’ve worked on and read about many actor runtimes, and sometimes there’s those which insist to take all the words “too literarily” and it becomes an un-understandable mess – “the actors are on a scene and have lines” and stuff (just tell me they are kept in memory and get messages)… it becomes too abstract and too attached to existing words. I think it’s totally fine to make up a name and specify it’s specific meaning in a context, and not let the real world parallels exact meanings be overly overlayed onto the concepts conveyed.

Behaviors: task.isCancelled

This is the intended (and implemented) behavior. I did attempt to make that clear in the proposal, perhaps it needs yet another explanation somewhere.

Behaviors: hasActiveTaskCancellationShield

That’s also correct. This just checks if we have the shielg flag set, regardless of cancelled status.

Naming

Yes, that’s why I’m more comfortable with a new name rather than imprecise attempts to explain it using the method names. All proposed names so far that attempt to explain the behavior miss some part of it, which will lead to move misunderstandings than a new term of art.

While this may seem pretty descriptive and good, it still is misleading because this doesn’t just prevent propagation but also changes the code within the scope of this block to not observe cancellation.

This isn’t just preventing propagation (which is the “cause child task to be cancelled as well”), so the name is missing to describe half of of the behavior of the API.

8 Likes