SE-0526: withDeadline

Hello Swift community,

The review of SE-0526: withDeadline begins now and runs through April 20, 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 the review manager via the forum messaging feature. When contacting 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 here.

Thank you,

Freddy Kellison-Linn
Review Manager

4 Likes

Can the proposal authors speak to the choice of wrapping the error cases? It definitely preserves the most information and is typed-error-compatible, but it's also more annoying to work with if you don't care about the distinction.

1 Like

Yeah it is a tradeoff... It would be very unfortunate to entirely lose a typed throw if you had one -- in general all "with... { ... }" functions try to preserve (or get updated to) typed throws. So the idea here was do to the same, though @FranzBusch and @Philippe_Hausler can speak more to this goal.

I will say though we're facing implementation issues with typed throws and had to delay adoption in some other APIs (in Concurrency library) until some inference issues become fixed with them. So most of Concurrency library still has not adopted typed throws at this point.

In my humble opinion, the real answer, that perhaps isn't as popular is, that we really would benefit from SomeError | DeadlineError. It is understandable for the language to not allow unions in general in the language, due to the complexity they cause, however I still -- personally at least -- find that we're really missing capability specifically for errors and it's forcing us into weird APIs like the one that can be seen here:

nonisolated(nonsending) public func withDeadline<Return, Failure: Error>(
  ...
  body: nonisolated(nonsending) () async throws(Failure) -> Return
) async throws(DeadlineError<Failure>) -> Return

Is this the proposal that'll make us reconsider future direction of typed throws and error unions?

10 Likes

Introduction

This proposal introduces withDeadline, a function that executes asynchronous operations with a composable absolute time limit. The function accepts a continuous clock instant representing the deadline by which the operation must complete. If the operation completes before the deadline, the function returns the result; if the deadline expires first, the operation is cancelled.

I find the Introduction a bit vague; it doesn't tell what the function will do after the operation is cancelled.

if the deadline expires first, the operation is cancelled.

I had to scan the whole proposal to find this crucial piece of information:

/// - Important: This function cancels the operation when the deadline expires, but waits for the
/// operation to return. The function may run longer than the time until the deadline if the operation
/// doesn't respond to cancellation immediately.

The Introduction should explicitly say:

if the deadline expires first, the operation is cancelled.
The function does not return immediately but waits for the operation
to throw an error or return result.
1 Like

I'm still concerned that withDeadline doesn't communicate the (reasonable, correct, but unintuitive) behavior that the task is cooperatively cancelled at the deadline. The discussion threads had several alternative names for this API, but withAutomaticCancellation(at deadline:...) seemed to have the most adherents. I see this is addressed in the "alternatives considered", but I still don't consider the issue settled.


DeadlineError doesn't capture the case that the operation is cancelled but subsequently succeeds (or fails in some manner undetectable to withDeadline's convention other than by throwing an error, e.g. returning false). So at least one additional case for Cause is required. I think that DeadlineError should be extended with the Return generic parameter, underlyingError should be removed, and Cause should be extended to cover all the cases:

enum Cause {
    case failureBeforeDeadline(OperationError)
    case successAfterDeadline(OperationReturn)
    case failureAfterDeadline(OperationError)
}

Note that this would imply that Return needs to be constrained to Sendable, in order to appear in the error type. I think that's necessary, but it's not very elegant. If the associated value of my proposed successAfterDeadline were omitted, the user would lose their ability to distinguish whether the operation had completed or not.


The API to return the current deadline as any InstantProtocol isn't actually useful, since there's no way to call either of InstantProtocol's APIs in that situation, without first downcasting to a known concrete type. That means that the deadline is effectively only useful to the code which set the deadline, which could more easily use a TaskLocal to pass itself a concretely-typed deadline. Using any InstantProtocol<Swift.Duration> is clearly better; that would at least allow advanced(by:) to be called, but I think that's still not actually useful. For anyone to do anything general-purpose with this value, they need to be able to cast it to the instant of a well-known clock.

Internally, this has to be implemented by calling sleep(until:tolerance:) on the user-provided clock. I expect that most user-provided clocks have to translate that to a call to ContinuousClock.sleep(until:tolerance:) anyway? But I guess it's theoretically possible that they use some external implementation such as a runloop timer or pthread_cond_timedwait or whatever…

An out-of-the-box idea is to provide an access method for the current deadline that does a kind of reopening of the user's clock existential (would have to be magic, but _openExistential exists as precedent); something like

Task.withCurrentDeadline { clock, instant in
    // this is actually a generic function, not a regular closure
    // it effectively has a signature like 
    //    f<C: Clock>(_ clock: Clock, _ instant: C.Instant)
}

But perhaps the right solution is just to provide the option to get the deadline in multiple ways:

public struct CurrentDeadline {
    // always available, but only useful to the calling code
    var instant: any InstantProtocol<Swift.Duration> { get }

    // non-nil if `withDeadline` was called with `ContinuousClock`
    var continuousClockInstant: ContinuousClock.Instant? { get }

    // or alternatively, just one accessor API:
    func instant<C: Clock>(_ clock: C) -> C.Instant?
}

extension Task where Success == Never, Failure == Never {
    // nil if we're not (dynamically) within a `withDeadline` call
    public static var currentDeadline: CurrentDeadline? { get }
}

making a CurrentDeadline type might make it easier to evolve the API if we add a hypothetical future protocol like RealtimeClock which provides functionality to convert to & from ContinuousClock; then if the user calls withDeadline with a RealtimeClock instance then the deadline can be available in multiple timebases simultaneously.

2 Likes

To the contrary, I think it's captured in the proposal right here under Detailed design:

/// - If deadline expires and operation completes successfully: Returns the operation's result.

I also agree withAutomaticCancellation(at: deadline) { ... } would read very well, and more clearly than withDeadline. Edit: withTaskCancellation(after: deadline) { ... } is good, too, as discussed in the pitch thread.

4 Likes

To add to what @ktoso already mentioned, I have shared a deadline method with Swift Server adopters for a few years now. My initial APIs looked very different to what is proposed here now. One of the differences was that I started off by just re-throwing the body's error without wrapping. Running this in production for a few years showed that this led to very hard-to-debug services. It was unclear why a method was suddenly cancelled and subsequently threw. Wrapping the error makes it very clear to the user that the passing deadline was the cause of this error. So even if we were to get union throws, I would still propose to wrap the error for the clearer semantics.

This is intentional. If the operation succeeds even in the case of cancellation, then I think it is very unexpected if that results in a thrown error. This applies to any method in general, not just withDeadline. Any method that notices cancellation can decide whether they want to throw an error or return something. It might be totally valid for them to return the current state of a computation. If withDeadline would wrap those returns into an error, it would entirely change how you would handle a call to the plain method vs the method wrapped in a withDeadline.

8 Likes

I remain worried that the naming here will result in lots of confusion. Particularly when used in combination with many of Apple’s callback-based APIs that don’t support cancellation.

7 Likes

The proposal looks overall good to me. However, despite how appealing and appropriate the name withDeadline may be, I would much prefer withTaskCancellationAfter(deadline: ...) and withTaskCancellationAfter(timeout: ...), as previously suggested. In my opinion, these fit much better into the existing _Concurrency APIs.

Also, what is the rationale behind separating the underlying error from the Cause enum? What about making Cause conform to Equatable?

6 Likes

These absolute deadlines are composable and nestable to any set scope of a deadline. This means that when more than one withDeadline is nested the minimum of the expiration is taken. If any nested cases are differing clocks the deadline is adjusted to the minimum by approximating the current deadline with the offset of the proposed expiration.

(emphasis added)

Does converting deadlines between clocks make sense?

For example, ā€œ5 secondsā€ on a SuspendingClock could be 5 seconds, 5 days, or 500 years on a ContinuousClock. Is it meaningful to talk about which is earlier?

1 Like

I agree that the rationale for naming doesn’t feel very convincing:

The cancellation behavior is part of the realities of how the language level concept of cooperative cancellation works and in reading the code at a potential call site it is more meaningful to convey the temporal nature of a deadline than to convey the cancellation being automatic.

On the contrary, I think the automatic cancellation is the key feature of this API. In particular, this cancellation happens at the moment a provided deadline is passed. There are several other plausible implementations for a deadline — raising a fatal error and halt the process, abandoning an unstructured task, or even popping up some sort of UI that tells the user things are taking a while. Are we sure that this particular deadline behavior is so overwhelmingly popular that any other behavior is clearly secondary to it?

Cancellation is already sort of a spooky action at a distance phenomenon where you need to inspect docs or source code to understand if a given API creates or consumes cancellation. Making the naming of this API a little more explicit about its cancellation via the name would help that situation somewhat.

Immediately the question that would be posed by folks unaware of this new API would be: "What automatic mechanism makes that cancellation happen?"

I think this would be made perfectly clear by the naming of the parameters. Spellings like after: .seconds(5) form a sentence with the method name that spell out the deadline behavior reasonably clearly: ā€œwith cancellation after 5 secondsā€

5 Likes

This is useful feedback, but I would request folks commenting on the "with cancellation after ..." API shape to not forget that the property to read the deadline off from a task is a hard requirement, so we have to have a name for this as well - it must work well with the API installing the deadline/timer/whatever-we'll-call-it, so I'd request also including this part of the API in the discussion when proposing alternative names

1 Like

Splitting the difference with withTaskDeadline would be better than withDeadline in my reading. It sticks to the ā€œdeadlineā€ terminology, which I think is suited to the purpose of the API, and the inclusion of ā€œtaskā€ makes it easier to read as only cancelling the task if the deadline passes. At least moreso than withDeadline which does not suggest it at all.

5 Likes

I think that reasoning works a lot better for non-Void returning functions than for void-returning ones. In the latter, the most common pattern for handling cancellation I've seen is this:

guard !Task.isCancelled else { return }

I've seen that way more often than try Task.checkCancellation(), and I don't think there's anything inherently wrong with it based on where the language stands today. The semantics of a function using this pattern are reasonable: the function does some work until completion, unless cancelled, in which case it returns early without completing the work.

There's no doubt in my mind people used to writing code with that shape are going to be surprised by try withDeadline { ... } not throwing a DeadlineError with .deadlineExpired. Worse, even: there seems to be no way of telling whether the cancellation was triggered or not in the first place[1].

I get the point that the caller can't tell if the work finished successfully or exited early due to cancellation unless it throws an error, and that such information is "lost" when callees do the guard !Task.isCancelled else { return } dance. But just because it can't tell which one it is, I don't think the API should bake in the assumption that no thrown error after cancellation means it executed to completion.

Considering the withDeadline API as a whole, it seems it has not one but two confusing wrinkles: not only is the deadline not enforced unless the underlying code checks for task cancellation, but even if the underlying code does handle cancellation, you can still get some rather unexpected behavior unless it's handling cancellation by throwing an error specifically.


  1. Other than, I guess, wrapping the code inside withDeadline in a withTaskCancellationHandler that sets a sentinel variable on cancellation. ā†©ļøŽ

7 Likes

I think that both deadline and currentDeadline work well with all suggested names so far.

1 Like

It seems that this is already termed expiration in the proposal?

I also noticed that it is typed as any InstantProtocol. I am happy to see the nod toward compatibility with different Clock types, but I am concerned that exposing just the Instant itself makes it more difficult to use Clock instances that have their own internal notions of time. Maybe such clocks should vend Instants that refer back to their owning clocks, but I perhaps it’s worth considering func expiration<C: Clock>(using clock: C) -> C.Instant?. The idea is that this could eventually leverage API on Actor to convert between the two actors’ preferred clocks.

Out of the alternative names this is perhaps the most reasonable in my view.

For the continuous clock and suspending clock yes, approximations between them do make sense in the regards that the caller must be in an "active" state of clock execution when invoking the function - therefore time must be relatively reasonable at the time of access and that means that the calculation of the new expiry of the deadline would generate a valid result. The only other way to deal with the generic nature of clocks (which compelling reasoning was presented not just for testing but also scenarios where a top level deadline construction would be inappropriate with certain clocks) is to hard-assert or failure. Which a hard-assert (crash) in my view would be rather abrasive to many applications. A failure (thrown error) would also be totally unexpected and likely be intractable in real world composition that don't know about each-other.

This dovetails to:

I would need some time to refine this; the suggested interface has some issues but... I could see where it might be possible to provide some sort of conversion routine and fallback to the estimation when that is not provided. However, to be very clear, I am not yet altering the proposal and will only be looking into that as an alternative to consider while the pitch is being run.

Maybe deadlines should only compose if they use the same Clock type.

withDeadline(..., clock: .continuous) {
    withDeadline(..., clock: MyCustomClock()) {
        // this code ignores the outer deadline because the clock types differ.
    }

    withDeadline(..., clock: .continuous) {
        // this code uses the composed deadline (min of outer and inner)
        // because they both use `ContinuousClock`.
    }
}

Sure; in a very technical sense it is an "option" to ignore it... but that would be quite surprising to me if I were using the API and I would guess folks would file many bugs around that.

Here’s an example to illustrate my point.

I'm using Ts for "time, as reported by the suspending clock" and Tc for "time, as reported by the continuous clock".

Given this (pseudo) code:

withDeadline(.now + 5s, clock: .suspending) {
    withDeadline(.now + 30s, clock: .continuous) {
        // ...
    }
}

And this timing:

  • At Ts=00/Tc=00: outer withDeadline called, requested deadline: Ts=05
  • At Ts=01/Tc=01: inner withDeadline called, requested deadline: Tc=31

At this point, the inner closure computes its ā€œcomposedā€ deadline. The suspending clock reports that the outer deadline is "4 seconds from now". The continuous clock reports that "4 seconds from now" is Tc=05.

  • At Ts=02/Tc=02: Device sleeps
  • At Ts=02/Tc=12: Device wakes

At this point, neither requested deadline has passed.

  • The outer closure's requested deadline was Ts=05 and the suspending clock is at Ts=02.
  • The inner closure's requested deadline was Tc=31 and the continuous clock is at Tc=12.

But, since the inner closure's composed deadline was Tc=05, and that time has passed, the inner operation is cancelled.

1 Like