[Second Review] SE-0526: withDeadline

Hello Swift community,

The second review of SE-0526: withDeadline begins now and runs through May 31st, 2026.

After the first review, the proposal authors have updated the proposal in response to the LSG's feedback:

  • The Task.currentDeadline API has been removed due to the ergonomic difficulties it presented when working with cross-clock deadlines.
  • The cross-clock deadline composition (via the Duration same-type constraint) has likewise been removed—composition now happens naturally via first-to-cancel wins.
  • The wrapping DeadlineError has been removed, and now withDeadline merely forwards the error thrown by its target operation.
  • CancellationError has gained a reason property which will be populated appropriately when cancellation is caused by deadline expiration.

A diff of the changes can be found here.

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

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Freddy Kellison-Linn
Review Manager

1 Like

What about the LSG’s first feedback item requesting deeper exploration of alternatives to the withDeadline name?

1 Like

The authors have elaborated on the discussion of alternatives and their preference for the withDeadline name (as well as precedent from other languages) in the Naming section under Alternatives considered. Naming is on-topic for this review, but rest assured that the LSG will consider all naming feedback from both reviews when it comes time for us to make a decision—so reviewers should not feel the need to re-raise their arguments about naming in this thread in order for them to be considered.

Does throws(Failure) allow for throwing Failure but also throwing CancellationError? Is there something else about the method signatures that says they can throw CancellationError? I assume this is carrying on existing behavior given it’s I think what Task initializers currently do.

I'm a lot happier with the error-handling overall now that withDeadline just passes through its original error; thank you for that.

I'm fine with the general idea of including a reason for cancellation in CancellationError. However, the proposal doesn't seem to specify any behavior around this reason. It should probably at least say the following:

  • A task that has been cancelled has a cancellation reason. Let C be the set of cancellation events of task T, and let C′ be the set of events E in C for which there is no other event in C that happens before E. Then the cancellation reason of T is chosen by the implementation from the reasons associated with the events in C′. This choice is arbitrary (presumably determined by whichever thread/subsystem wins the race to cancel the task) but consistent for any given execution of the program.

    Note that, in particular, if there is a cancellation event of T which happens before all other cancellation events, the cancellation reason of T is always the reason associated with that event.

  • When standard library APIs such as Task.checkCancellation throw a CancellationError because the current task has been cancelled, the error always carries the cancellation reason for the current task.

  • When a task is cancelled, any sub-tasks of that task are immediately cancelled with a reason equal to the cancellation reason of the task.

I also have some specific concerns about the Reason type specified in the proposal:

  public enum Reason {
    case taskCancelled
    case deadlineExpired
    case custom(String)
  }

First off, is there a reason this needs to be a separate type from CancellationError? It feels like this design makes catching specific kinds of cancellation error more difficult, but maybe that's intentional.

Second, the taskCancelled case name seems very generic. The task is cancelled in all of these cases. I do understand that the default, no-reasons case is hard to name, though. Maybe userRequested?

Third, I'm very concerned about the custom case; this feels like an open invitation to smuggle arbitrary data through cancellation events, just stringly-typed, and I'm not sure we want to support that. It's also likely to introduce real overhead even if never used: if Reason can be compressed to 2 or 3 bits (like if it's just a handful of payload-less cases), we can store it inline in the existing status field, but the String payload means we presumably need to allocate space for a Reason value ahead of time in every Task just in case it's cancelled with a custom reason. (And of course that reason has to be copied for every child task cancellation.)

6 Likes

I believe withDeadline is now just passing through the result returned / error thrown by the closure you pass to it, so throws(Failure) is correct. If you want your closure to react differently to cancellation, like wrapping it up as a different error type, you need to catch CancellationError within the closure.

3 Likes

As a final note, the disappearance of any way to programmatically query the current deadline seems like a real loss. It's true that arbitrary clocks are hard to work with: you can't usually compare or compose deadlines in different clocks, and a lot of clocks are only meaningful within a specific domain[1]. But there are some specific clocks that are exceptions to that; for example, ContinuousClock instants can logically be mapped to UTC (although I don't think this API currently exists anywhere?) and passed over the network. Even if an arbitrary function can't do anything with the fully general set of deadlines that might be attached to the current task, it could probably still benefit from asking about deadlines in specific clocks, and that does seem supportable.


  1. For example, a test-driven clock only makes sense in the context of that test, and the system suspending clock only makes sense for the current system. ↩︎

8 Likes

I thought we agreed on removing the custom(String) case in this discussion on the proposal update @Philippe_Hausler? (And may revisit it separately if necessary)

2 Likes

Absolutely agreed, we must have a way to query the deadline; Without ability to query deadlines this feature cannot be used cross process which is an important thing we care about for IPC and even network use-cases.

I'm surprised by this change, seems I missed this when reviewing the update? :shaking_face:

Yeah this would be workable and we could offer perhaps it using a shape like this:

Task.currentDeadline // defaults to ContinuousClock.Instant
Task.currentDeadline(clock: .continuous)

since withDeadline already defaults to using ContinuousClock

3 Likes

umm yes I thought I removed that.

1 Like

Querying the deadline is not strictly forbidden from any future development, but as it currently stands the overhead for being able to query that in any sort of useful manner is just not tenable for a public interface at this time. Exposing it as an any InstantProtocol is quite honestly less than helpful.

What about querying if there’s a deadline associated with a specific clock?

3 Likes

What about nil? Then the error would say “this task was cancelled but there is no specific reason for the cancellation”

1 Like

Glad to hear this since it also makes the implementation and API a lot simpler.

I suggested the reason based design for two reasons:

  1. From existing usage of withTimeout/Deadline based APIs users want to understand why their work threw a CancellationError. The first iteration wrapped their error to indicate this which proved to be too unergonomic on the API. Hence, this iteration introduces the task cancellation reason based design.
  2. I personally would like to follow up this proposal with a general cancellation reason design that changes all the cancel methods (e.g. Task.cancel) to take a reason and exposes the cancellation reason on task itself e.g. Task.cancellationReason. This is a feature request that I have heard multiple times now since it allows users to map their domain specific cancellation reason into Swift's cancellation pattern

userRequested or maybe default or even making the reason optional could work here. I agree that taskCancelled is too generic.

As @ktoso pointed out, our intention was to not pitch this here but keep the enum @nonexhaustive to leave the door open for future evolution. As I said in my first paragraph, I personally think we should explore a more general cancellation reason concept, including a custom case. It is a common need to understand why a task was cancelled, and the reason allows us to encode that. Having said that, this should really be in a separate follow-up proposal.

So the primary issue with this approach is that in reality you need to walk the current deadlines (not just one). Since they are nested it means that the comparison needs to be applied to all of the deadlines that are active and while walking that the type is generic. The only way currently to do that is to use a "visitor" pattern with the visitor having a function level generic to allow each clock to be passed in.

So it would at best have to be something similar to this:

protocol DeadlineVisitor {
  mutating func visit<C: Clock>(_ deadline: C.Instant, tolerance: C.Instant.Duration?, clock: C)
}

extension Task where Success == Never, Failure == Never {
  static func visitCurrentDeadlines(_ visitor: some DeadlineVisitor) { ... }
}

Which is not very usable in my opinion. I am willing to be convinced that we could somehow do better than that, but I have yet to figure out a better way to traverse all of the applied deadlines.

The root issue is that the most outer of a nested deadline may be the most constrained expiration. This would mean that if we just swapped to a current singular deadline it would end up not representing the actual expiration. So to be able to calculate the effective minimum (other than the current implementation relying on the clock to provide the sleep) we would need to walk all of those applied deadlines and let the developer trying to query that determine what to do. The issue then becomes; can that actually be useful? I am not convinced that even with the more complicated visitor pattern it would even be possible to use that to extract a minimum, even an approximated one.

Looking at the bigger picture here; we want this for communicating deadlines associated with cross process events? If you have the setup of process A calling an IPC to process B, the process A function would likely need/want a deadline applied, but I would also posit that the top level corresponding side of process B would as well; but in those cases isn't the cancel the important bit? not per-se the value of the deadline. Which the API as proposed (without querying the stack of current deadlines) does let happen via withTaskCancellationHandler.

There's no need for such complex heroics; All we need to offer is a way to read with a specific clock:

extension Task { 
  var deadline: ContinuousClock.Instant? -> { deadline(clock: ContinuousClock()) }

  func deadline<C: Clock>(clock: Clock) -> C.Instant? { ... }
}

Where we lookup the deadline identified by this clock (if a custom clock, not the default ones, we have to take into acount its identity -- so clocks may need to get Identifiable conformance).

Note that when nesting deadlines, we never store the "in 3 seconds" but always should store the closest instant for this specific clock. So there's always a concrete instant for the specific clock that we are comparing with. There's no need to compare across clocks.

Propagation across IPC will have to handle clocks they want to propagate, but that's okey and reasonable requirement for propagation -- various sytems may choose to propagate differently, and they may just open this up like Instrument does in swift-distributed-tracing, for users to propagate their WeirdCustomClock etc.

Sadly the instance of a clock matters because custom clocks may have their time per instance, like a test clock...

1 Like

It does feel like an oversight in the Clock protocol that there’s no concept of equality or identity for clock instances. (It also feels like maybe the clock ought to be involved in comparing instants.) I don’t think we can do anything about that now, unfortunately. We could require an additional conformance when calling currentDeadline, though.

4 Likes

This time around -1 and two big concerns:

  1. Querying the active deadline is a must, you want to pass this on to remote services
  2. The tolerance = nil is still the wrong default for almost everything and everybody. It has caused outages for us and it will continue to do so. Defaulting to this is a security vulnerability. (It's common to have long-ish deadlines in the minutes. And the timer coalescing will coalesce a lot of seconds into one exact instance in time where your application will suddenly have a surge of work to do)
2 Likes

I think your claim is too broad here. Apple spent a decade getting app developers away from precise timer callbacks in order to better optimize power usage and overall system performance, especially on mobile devices. Coalescing a lot of work into narrow periods is exactly what you want when you're concerned with balancing maximum CPU performance with minimum battery burn.

And isn't nil really just saying "leave it to the system", allowing mobile devices to optimize around the performance / battery balance while other systems optimize in other ways? (Looking at the implementation it seems this is up to the specific Clock, which is more concerning, since it's not a guarantee of any particular behavior.)

Can you outline your security concerns here? I can see DDoS vector where an attacker can take advantage of fixed time intervals to spike CPU usage artificially, but I'd think a nil default actually works against that. By allowing the systems to perform their own scheduling, they can more intelligently track work scheduled at specific times and spread it out. What default would you like to see?

3 Likes

I generally agree, although I’m not sure how it could ever not be up to the clock. We cannot guarantee precise timer behavior in any event.

2 Likes