SE-0520: Discardable result use in Task initializers

Hello, Swift community!

The review of SE-0520: Discardable result use in Task initializers begins now and runs through March 30, 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 by email or DM. When contacting the review manager directly, please put "SE-0520" in the subject line.

Trying it out

The toolchain with the latest implementation is currently building - @ktoso will update the review thread with the toolchain links for macOS, Linux, and Windows when they're ready.

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:

swift-evolution/process.md at main ยท swiftlang/swift-evolution ยท GitHub

Thank you,

Holly Borla
Review Manager

17 Likes

Overall +1.

I'm just concerned about one point:

instead, this proposal will introduce a new bespoke warning mechanism which is able to trigger ...

I assume magic like this that complicates the compiler is generally not preferred?

Yeah generally it's better to model things with general features, agreed.

The way I've implemented this is with an attribute that basically says "if there is an parameter with a closure, and it throws, assume we must not discard the result even though it's not re-throwing". It COULD be generalized in the future to make it into an official attribute as such behavior is common for any "future-like" type, like Task. So it's kinda niche, but not necessarily tied to Task, and I could see other libraries making use of it (basically an extension of @discardableResult).

So I wouldn't mind the extra work to pitch an official spelling for this in the future. Since we're a bit late for the 6.4 release I'd suggest reviewing this independently though.

8 Likes

Here's toolchain links, built freshly off Sign in to GitHub ยท GitHub

Is it not possible to do this via the existing overloads, from before typed throws? Admittedly that doesn't account for throws(Never), but that's a bit of an edge case anyway.

(I am in favor of the proposal in general.)

Part of the goal here is to reduce the number of overloads. It's gotten so bad that we had to use gyb to generate the sheer number of overloads of task initializers and task groups in the past.

Adopting typed throws has a side-benefit of being able to kill off this gyb generation and converge on less overloads: the de-gybification is in progress over here, and isn't really tied to the proposal semantics itself, but since you asked why we're doing it this way and not another way, that's a contributing factor.

3 Likes

I continue to believe that you don't generally want to discard task handles, aside from possibly "dispatch_async-like" cases (synchronous after an initial actor hop).

That said, this is clearly better than the status quo, and nothing prevents adding warnings in more cases in future, so definitely +1.

3 Likes

If this does eventually get an official spelling, perhaps it could take the form of a where clause on @discardableResult. So the Task initializer could have @discardableResult(where Failure == Never).

But since for now, this proposal introduces magic, I think it could be worthwhile to extend the magic to also account for non-Void and non-@discardableResult closures. That is, there could be another compiler-internal attribute that says "if there is a parameter with a closure, and the closure returns Void or it has an implicit return from a single expression that calls a @discardableResult function".

Perhaps "where Failure == Never && Success == Void" to ensure that not only errors but results themselves are never lost.

Could this work:

to only have discardableResult for Success == Void && Failure == Never combination?

What was the reason for having where Failure == any Error at all, could it be anything else?

1 Like

Surely Failure could be a concrete Error type.

This seems to work desirably (mocking the Task API):

struct Test<Success, Failure: Error> {}

extension Test where Success == Void, Failure == Never {
    @discardableResult init(operation: () -> Success) {}
}

extension Test where Failure == any Error {
    init(operation: () throws -> Success) {}
}

enum MyError: Error { case error }

Test { () -> Void in } // void, no throw, โœ… no warning
Test { () -> Int in 0 } // int, no throw, ๐Ÿ”ถ unused warning
Test { throw MyError.error } // void, throw ๐Ÿ”ถ unused warning
Test { throw MyError.error; return 0 } // int, throw ๐Ÿ”ถ unused warning

I think we want the latter extension to use typed throws like so:

extension Test {
    init(operation: () throws(Failure) -> Success) {}
}

But that also compiles just like your mock, and the point remains and I agree only Task<Void, Never>.init should be @discardableResult.

But there's a practical question of whether the code churn of having to type _ = ... in non-throwing cases too would be too large. For instance, one practical issue with @discardableResult is that it doesn't get forwarded in cases like:

let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
Test { // โš ๏ธ Result of 'Test<Success, Failure>' initializer is unused
    continuation.yield(42)
}

forcing to prepend either the inner or the outer expression with _ =.

I'd say neither of those, but something more like:

try continuation.yield(42).checkResult()

which will throw on yield errors.

That's not my point. In this case, my code wanted to form a Task<Void, Never> (or your mock Test<Void, Never>) from a non-throwing expression, but due to yield(_:) returning a @discardableResult itself, the type system actually made it a Task<AsyncStream<Int>.Continuation.YieldResult, Never>, which by your proposed new rules would no longer be @discardableResult itself.

I also think the API would be clearest if only Task<Void, Never> was @discardableResult, and all other types of unstructured Tasks (throwing, or non-throwing but value-returning) would give the warning unless explicitly discarded.

But I'm trying to point out that there may be cases where the community may still want to keep Task<_, Never> also @discardableResult for all other Success types than Void, as well.

Yeah, I see. One example I had in mind was items.removeLast() which returns the item which would have to be silenced with _ = if it's unwanted.

In case of your example it could be silenced with try! continuation.yield(42).checkResult() to not get that throw propagated outside (with the side benefit that if it does throw โ€“ you'd quickly know about that).

I'd love that as well... even to the point if discardableResults returning functions were not be able participating in the "single (or last) expression don't need return" machinery and require explicit return statements to make intent clear (as pitched here)


Thinking out loud: what if discardable was on type? And thus propagable along with the type..

Task { // inferred as Task<discardable Int, Never>
    items.removeLast() // `discardable Int`
}

along with something akin to:

extension Task where Success is discardable, Failure == Never {
    @discardableResult init(...) { ... }
}

Existing precedents from other languages of embedding discardability in types
struct [[nodiscard]] SomeType {
    int x;
};
SomeType foo() {
    return SomeType();
}
void bar() {
    auto proc = []{ return foo(); };
    proc(); // ๐Ÿ”ถ Warning: Ignoring return value of function declared with 'nodiscard' attribute
}

An unfortunate development here is that we're not able to use typed throws in this API (yet), as doing so has caused a significant flood of source compatibility regressions. We'll have to solve these implications of typed throws before this API will be able to adopt it. (typed throws adoption was reverted).

This discussion should continue about just "throws" but the spirit of the proposal remains the same.

1 Like