The third review of SE-0526: withDeadline begins now and runs through July 12th, 2026.
After the second review, the proposal authors have updated the proposal in response to the LSG's feedback:
The CancellationError.Reason.custom(String) case has been removed and the .taskCancelled case renamed to .userRequested.
Task.hasActiveDeadline and Task.activeDeadline<C: Clock & Identifiable>(for clock: C) -> C.Instant? APIs have been introduced for querying the deadline environment, and stdlib clocks have gained an Identifiable conformance.
A Task.cancellationReason API has been introduced.
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 the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.
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 at
Overall, this looks like a much cleaner proposal than the previous ones.
The proposal says that the closure is nonisolated(nonsending) so that it can access local state. I believe this will be the first time this is used for this purpose in a stdlib API? Previously we've used @_inheritActorContext and isolated(any) to declare similar properties? By my understanding, nonisolated(nonsending) doesn't limit what can be passed to the parameter, it only affects the default behavior of a closure literal passed to the function. Does that matter? Or is passing a closure with different behavior always more strictly constrained and therefore always safe? And the current implementation of withDeadline does actually send the closure (using task groups) before calling it. Is the fact that the task group blocks the local context enough to make that send technically-safe? Or could it be detected in some way (task locals? executor preferences?). If it is safe and undetectable, does that imply that the current task group API could be relaxed in some way? Or is it all the other special cases of this API that make this usage safe?
I really like this identifiable Clock idea, but it seems underspecified in the proposal.
Two questions I immediately had but didn’t find answers to:
Do two instances of ContinuousClock (or SuspendingClock) always have the same identity? Might that change in the future?
How does the coalescing work? The proposal describes an example where an outer and an inner ContinuousClock resolve to a single deadline, but doesn’t explain how activeDeadline(for:) actually performs that operation, or how third-party clocks can participate.
Per the nonisolated(nonsending)@FranzBusch would be the best one to speak to that part of the proposal. From my understanding this way of writing it is the same as the previous version and is the stable, non-underscored spelling of that type of functionality. But perhaps Franz has some additional commentary on that.
The task groups are resuming within the scope of the async execution, so that in conjunction with careful handling of the values prevent us from making it unsafe; leaving the exposed surface safe to mark as the nonisolated version.
For the record, we likely can't change the identity of clocks that are abi; and particularly any implementation of ContinuousClock and SuspendingClock will always have equal instance identification; i.e. ContinuousClock().id == ContinuousClock().id and likewise for SuspendingClock is an invariant that cannot change.
@_inheritActorContext only applies when the function-type parameter is either @Sendable or sending, and the usual closure isolation rules apply. The resulting isolation is static. By contrast, a nonisolated(nonsending) closure that is not statically isolated executes on the calling context's executor.
I haven't looked at the updated implementation yet, but using a nonisolated(nonsending) closure in a task group could be problematic.
nonisolated(nonsending) is the way to spell this guarantee and we've been missing for a bunch of releases. All other spellings don't give the same strong guarantee. The rest of the stdlib is silently moving away from isolated ... = #isolated to nonisolated(nonsending) because the stronger guarantees it provides. These changes do not go through SE review because they are functionally the same, it is just better optimized / guaranteed lack of actor hopping.
The task group closure always should have been nonisolated(nonsending) but we were not able to express this. Now we can, yet sadly due to source compatibility problems we've not yet adopted it in that specific API. It is a general goal to move everything in stdlib that has the "runs in caller" guarantee over to nonisolated(nonsending).
There are two pieces here. As @ktoso alluded to the withTaskGroup body closure should be updated to nonisolated(nonsending) as it does inherit the callers isolation. However, in the current implementation we do spawn two child tasks to race the user's operation with the Task.sleep. However, we are dynamically ensuring that the isolation of the user's closure is upheld in the child task. Below is a small snippet how such a dynamic isolation inheritance into a child task can be achieved.
Dynamic isolation inheritance
This uses a few intermediate methods to shape the closure into a @escaping and @Sendable closure.
I meant the addTask case and had a similar idea in mind with the #isolation macro. This also reminded me that we should probably update the documentation for the #isolation macro, since it now also supports dynamic isolation.
Anyway, I don’t want to derail this thread any further.
With cancellation shields at least the idea was originally that you could potentially want to ignore a shield... which would mean there is one, but it's not active. I'm not sure we need to be following the same rationale here though, in practice you'd be doing a cancellation shield to ignore a deadline rather than anything with the deadline itself after all.
So I'm actually leaning to just Task.deadline, as it was originally proposed.
And similarily Task.hasDeadline, which we do need because it means "has a deadline on any clock", so we can't not have this because we can != nil compare (we don't know what clock might have the deadline potentially)
As someone who’s not too deeply following this discussion, I have to say that activeDeadline is much clearer to me than just deadline.
The word “active” tells me that it’s about state setup by previous code. Without looking at the signature it makes me expect to return an optional Deadline or so.
On the other hand, just “deadline” could also create another deadline object.
The one part that active adds to the meaning is that it refers to the fact that the deadline may be composed from a minimum of two applied deadlines, else it might be confusing for developers to have a deadline and then fetches the deadline and they differ. Active in that case infers there may be some level of calculation.
This would make the Task.cancellationReason API a bit more problematic—I don’t think we can justify a nested optional here so we’d either need to flatten the “not cancelled” and “cancelled without reason” states into .none which seems less than ideal, or come up with a different shape for the return type (or remove the API).