From 953fddc40f49063907632bdd208388a0d84f2ce9 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Thu, 29 Aug 2024 06:41:57 -0400 Subject: [PATCH 01/15] WIP - proposal skeleton, motivation --- .../0NNN-unstructured-task-error-handling.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 proposals/0NNN-unstructured-task-error-handling.md diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md new file mode 100644 index 0000000000..0e6f22a58d --- /dev/null +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -0,0 +1,51 @@ +# Feature name + +* Proposal: [SE-NNNN](0NNN-unstructured-task-error-handling.md) +* Authors: [Konrad 'ktoso' Malawski](https://github.com/ktoso), [Matt Massicotte](https://github.com/mattmassicotte) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [swiftlang/swift/pull/74110](https://github.com/swiftlang/swift/pull/74110) +* Upcoming Feature Flag: *if applicable* `MyFeatureName` +* Review: ([pitch](https://forums.swift.org/t/pitch-non-discardable-throwing-tasks/74138)) + +## Introduction + +This proposal modifies the API of `Task` to adopt typed throws and changes the default so it is no longer possible to passively ignore any thrown errors. + +## Motivation + +The purpose of the `Task` APIs is to capture the outcome of an asynchronous operation, either as a resulting value or an error. The actual error type, however, is not available to callers that access this result via the `value` accessor. This is exactly the kind of problem that [typed throws][https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md] can address. + +Additionally, the `Task` creation APIs are all annotated with `@discardableResult`. This makes it extremely easy for the code creating the task to unintentionally ignore errors thrown in the body. This default has proven to be surprising, error-prone, and difficult to debug. + +## Proposed solution + +TBD + +## Detailed design + +TBD + +## Source compatibility + +TBD + +## ABI compatibility + +TBD + +## Implications on adoption + +TBD + +## Future directions + +TBD + +## Alternatives considered + +TBD + +## Acknowledgments + +TBD From ad370e111c6dd86d0f9816af45ea41a4cf320533 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 30 Aug 2024 07:02:31 -0400 Subject: [PATCH 02/15] WIP - filling in more motivation --- .../0NNN-unstructured-task-error-handling.md | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 0e6f22a58d..db97860ead 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -10,13 +10,48 @@ ## Introduction -This proposal modifies the API of `Task` to adopt typed throws and changes the default so it is no longer possible to passively ignore any thrown errors. +This proposal modifies the API of `Task` to adopt typed throws and changes the +default so it is no longer possible to passively ignore any thrown errors. ## Motivation -The purpose of the `Task` APIs is to capture the outcome of an asynchronous operation, either as a resulting value or an error. The actual error type, however, is not available to callers that access this result via the `value` accessor. This is exactly the kind of problem that [typed throws][https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md] can address. - -Additionally, the `Task` creation APIs are all annotated with `@discardableResult`. This makes it extremely easy for the code creating the task to unintentionally ignore errors thrown in the body. This default has proven to be surprising, error-prone, and difficult to debug. +The purpose of the `Task` APIs is to capture the outcome of an asynchronous +operation, either as a resulting value or an error. +The actual error type, however, is not available to callers that access this +result via the `value` accessor. +This is exactly the kind of problem that [typed throws][] can address. + +```swift +let task = Task { + throw MyError.somethingBadHappened +} + +do { + _ = try await task.value +} catch { + // type information has been lost and error is now `any Error` +} +``` + +Additionally, all the `Task` creation APIs are annotated with +`@discardableResult`, including those that permit failure. +This makes it extremely easy for the code creating the task to +unintentionally ignore errors thrown in the body. +This default has proven to be surprising, error-prone, and difficult to debug. + +```swift +Task { + try first() + try second() + try third() +} +``` + +Because the creating site has not captured a reference to the task, +this code is ignoring any failure. This *could* be the author's intention, +but it not really possible to determine this by looking the code. + +[typed throws]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md ## Proposed solution From 970460f763452ced0714ff4cf45a865b32f194b6 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:50:19 -0400 Subject: [PATCH 03/15] WIP - more detailed design --- .../0NNN-unstructured-task-error-handling.md | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index db97860ead..1fd611df55 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -5,7 +5,7 @@ * Review Manager: TBD * Status: **Awaiting review** * Implementation: [swiftlang/swift/pull/74110](https://github.com/swiftlang/swift/pull/74110) -* Upcoming Feature Flag: *if applicable* `MyFeatureName` +* Upcoming Feature Flag: `TaskInitTypedThrows` * Review: ([pitch](https://forums.swift.org/t/pitch-non-discardable-throwing-tasks/74138)) ## Introduction @@ -55,11 +55,70 @@ but it not really possible to determine this by looking the code. ## Proposed solution -TBD +We propose two changes to the `Task` initialization functions to address these problems: + +- adopt typed throws +- remove the use of `@discardableResult` unless `Failure` is `Never` ## Detailed design -TBD +`Task` now has new initializers and matching detached variants. In the case where `Failure` is `Never`, these do not permit a throwing body and preserve the ability to ignore the created `Task` instance. + +```swift +extension Task where Failure == Never { + @discardableResult + @_alwaysEmitIntoClient + public init( + priority: TaskPriority? = nil, + @_inheritActorContext @_implicitSelfCapture operation: __owned sending @escaping @isolated(any) () async -> Success + ) { + // ... + } + + @discardableResult + @_alwaysEmitIntoClient + public static func detached( + priority: TaskPriority? = nil, + operation: __owned sending @escaping @isolated(any) () async -> Success + ) -> Task { + // ... + } +} +``` + +However, for a non-`Never` `Failure`, the `throws` cause exposes the type and the `@discardableResult` is dropped. + +```swift +extension Task { + @_alwaysEmitIntoClient + public init( + priority: TaskPriority? = nil, + @_inheritActorContext @_implicitSelfCapture operation: __owned sending @escaping @isolated(any) () async throws(Failure) -> Success + ) { + // ... + } + + @_alwaysEmitIntoClient + public static func detached( + priority: TaskPriority? = nil, + operation: __owned sending @escaping @isolated(any) () async throws(Failure) -> Success + ) -> Task { + // ... + } +} +``` + +The `value` property used a typed throws clause to expose the `Failure` at the site of access. + +```swift +extension Task { + public var value: Success { + get async throws(Failure) { + // ... + } + } +} +``` ## Source compatibility From 666c57f19fe06f89777b06ca46cec299bb5218af Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Sat, 14 Sep 2024 07:11:26 -0400 Subject: [PATCH 04/15] Filling more out --- .../0NNN-unstructured-task-error-handling.md | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 1fd611df55..4f9e0436da 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -62,7 +62,10 @@ We propose two changes to the `Task` initialization functions to address these p ## Detailed design -`Task` now has new initializers and matching detached variants. In the case where `Failure` is `Never`, these do not permit a throwing body and preserve the ability to ignore the created `Task` instance. +`Task` now has new initializers and matching detached variants. +In the case where `Failure` is `Never`, +these do not permit a throwing body and preserve the ability to ignore the +created `Task` instance. ```swift extension Task where Failure == Never { @@ -86,7 +89,8 @@ extension Task where Failure == Never { } ``` -However, for a non-`Never` `Failure`, the `throws` cause exposes the type and the `@discardableResult` is dropped. +However, for a non-`Never` `Failure`, the `throws` cause exposes the type +and the `@discardableResult` is dropped. ```swift extension Task { @@ -108,7 +112,8 @@ extension Task { } ``` -The `value` property used a typed throws clause to expose the `Failure` at the site of access. +The `value` property used a typed throws clause to expose the `Failure` at +the site of access. ```swift extension Task { @@ -122,24 +127,46 @@ extension Task { ## Source compatibility -TBD +This proposal should not affect the behavior of existing code. -## ABI compatibility +However, it does intentionally introduce a warning into code that is +ignoring errors produced by a `Task`. +The developer's intention in this situation is inherently ambigous, +and one of the purposes of this proposal is to resolve this ambiguity. +The expectation is this will catch real mistakes. -TBD +Explicitly expressing the intention to actually ignore errors is also +very straightforward: -## Implications on adoption +```swift +let = Task { + throw MyError.somethingBadHappened +} +``` -TBD +## ABI compatibility -## Future directions +This proposal does not change how any existing code is compiled. -TBD +## Implications on adoption + +No new types are being introduced, and the APIs that require change are all +annotated with `@_alwaysEmitIntoClient`. +This propsoal can be implemented purely in the compiler. ## Alternatives considered -TBD +It is completely possible to adopt typed throws for these APIs without +changing the behavior of the throwing case. +Further, introducing a warning in cases where ignoring errors is intentionally +could be an annoyance. + +However, choosing a surprising and potentially error-prone behavior as the +default goes against Swift's general philosophy of safety. +Changing this default feels like a much better balance, especially since +re-expressing the existing behavior involves such a familar language pattern. ## Acknowledgments -TBD +Thanks to John McCall for engaging with the community on this topic and helping +to articulate the history and reasoning around the design. From 285287ab240409e668f092cd2ade8deb58a6d9b6 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 18 Sep 2024 13:59:07 +0900 Subject: [PATCH 05/15] Update 0NNN-unstructured-task-error-handling.md --- .../0NNN-unstructured-task-error-handling.md | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 4f9e0436da..a162ffc21a 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -1,4 +1,4 @@ -# Feature name +# Improved error handling in unstructured Task initializers * Proposal: [SE-NNNN](0NNN-unstructured-task-error-handling.md) * Authors: [Konrad 'ktoso' Malawski](https://github.com/ktoso), [Matt Massicotte](https://github.com/mattmassicotte) @@ -10,16 +10,19 @@ ## Introduction -This proposal modifies the API of `Task` to adopt typed throws and changes the -default so it is no longer possible to passively ignore any thrown errors. +This proposal modifies the API of `Task` to adopt typed throws and makes it easier +to spot when an unstructured task is throwing and would have potentially "hidden" a thrown error accidentally. ## Motivation -The purpose of the `Task` APIs is to capture the outcome of an asynchronous -operation, either as a resulting value or an error. -The actual error type, however, is not available to callers that access this -result via the `value` accessor. -This is exactly the kind of problem that [typed throws][] can address. +The purpose of the unstructured tasks is to create a new asynchronous context in which computation may happen. +Unstructured tasks–unlike their structured cousins (async lets, and task groups)– do not have to be awaited on +and their results and thrown errors are simple to discard by just not storing and not awaiting on the created task's `.value`. + +Tasks are typed using both the `Success` and `Failure` however, until the recent introduction +of [typed throws][] to the language, the `Failure` type could only ever have been `Never` or `any Error`. + +For example, the following snippet showcases how we lose the error type information when throwing within a task: ```swift let task = Task { @@ -49,7 +52,8 @@ Task { Because the creating site has not captured a reference to the task, this code is ignoring any failure. This *could* be the author's intention, -but it not really possible to determine this by looking the code. +but it not really possible to determine this by looking the code. The community has frequently +requested this to be rectified, such that the error ignoring is [typed throws]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md @@ -62,42 +66,56 @@ We propose two changes to the `Task` initialization functions to address these p ## Detailed design -`Task` now has new initializers and matching detached variants. -In the case where `Failure` is `Never`, -these do not permit a throwing body and preserve the ability to ignore the -created `Task` instance. +`Task` currently has two initializers and matching detached variants: `.init` and `.detached`. + +We propose to adjust these initializers by: + +1) Keeping the the the non-throwing overloads of those APIs with their `@discardableResult` annotation. + +This is because while it is common to create fire-and-forget tasks, like `Task { await doSomething() }`, +while ignoring the result of the task. ```swift +// Current signatures + extension Task where Failure == Never { @discardableResult - @_alwaysEmitIntoClient public init( priority: TaskPriority? = nil, - @_inheritActorContext @_implicitSelfCapture operation: __owned sending @escaping @isolated(any) () async -> Success + operation: sending @escaping @isolated(any) () async -> Success ) { // ... } @discardableResult - @_alwaysEmitIntoClient public static func detached( priority: TaskPriority? = nil, - operation: __owned sending @escaping @isolated(any) () async -> Success + operation: sending @escaping @isolated(any) () async -> Success ) -> Task { // ... } } ``` -However, for a non-`Never` `Failure`, the `throws` cause exposes the type -and the `@discardableResult` is dropped. +2) Adjust the `throwing` variants of those APIs by: + +- removing the `@discardableResult` attribute +- adopting typed throws + +We argue that the fact that accidentally forgetting to handle an error is more common and "risky", +than forgetting to obtain the result value of an unstructured task. If a task is created and it's +result is importand to handle, developers naturally will store and await it. However, ignoring errors +even in the simple "fire-and-forget" task case, may yield to unexpected and silent dropping of errors. + +Therefore we argue that the discardable result behavior need only be dropped from the throwing versions of these APIs. ```swift +// Proposed signatures + extension Task { - @_alwaysEmitIntoClient public init( priority: TaskPriority? = nil, - @_inheritActorContext @_implicitSelfCapture operation: __owned sending @escaping @isolated(any) () async throws(Failure) -> Success + operation: sending @escaping @isolated(any) () async throws(Failure) -> Success ) { // ... } @@ -127,16 +145,14 @@ extension Task { ## Source compatibility -This proposal should not affect the behavior of existing code. +This proposal is source compatible. However, it does intentionally introduce a warning into code that is -ignoring errors produced by a `Task`. -The developer's intention in this situation is inherently ambigous, -and one of the purposes of this proposal is to resolve this ambiguity. -The expectation is this will catch real mistakes. +ignoring errors that may be thrown by awaiting on an unstructured `Task`. + +If the developer's intent was truly to ignore the task handle and potentially thrown error, +they should explicitly ignore it (which will silence the warning): -Explicitly expressing the intention to actually ignore errors is also -very straightforward: ```swift let = Task { @@ -144,15 +160,14 @@ let = Task { } ``` -## ABI compatibility +The should imrprove code quality by making it more obvious when potential errors are being ignored. -This proposal does not change how any existing code is compiled. +## ABI compatibility -## Implications on adoption +This proposal is ABI additive. -No new types are being introduced, and the APIs that require change are all -annotated with `@_alwaysEmitIntoClient`. -This propsoal can be implemented purely in the compiler. +APIs that require change are all annotated with `@_alwaysEmitIntoClient`, +so there is no ABI impact on changing them. ## Alternatives considered From 92cf606a33469b648519e5960d629cc7905b5ef0 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Sat, 21 Sep 2024 06:42:19 -0400 Subject: [PATCH 06/15] Wording and formatting pass --- .../0NNN-unstructured-task-error-handling.md | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index a162ffc21a..657488aaf5 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -10,19 +10,24 @@ ## Introduction -This proposal modifies the API of `Task` to adopt typed throws and makes it easier -to spot when an unstructured task is throwing and would have potentially "hidden" a thrown error accidentally. +This proposal modifies the API of `Task` to adopt typed throws and makes it +more difficult to ignore thrown errors accidentially. ## Motivation -The purpose of the unstructured tasks is to create a new asynchronous context in which computation may happen. -Unstructured tasks–unlike their structured cousins (async lets, and task groups)– do not have to be awaited on -and their results and thrown errors are simple to discard by just not storing and not awaiting on the created task's `.value`. +The purpose of unstructured tasks is to create a new asynchronous context in +which computation may happen. +Unlike the structured constructs (async lets and task groups), +unstructured tasks to not have to be awaited. +Their results and thrown errors are simple to discard by just not storing and +not awaiting on the created task's `.value`. -Tasks are typed using both the `Success` and `Failure` however, until the recent introduction -of [typed throws][] to the language, the `Failure` type could only ever have been `Never` or `any Error`. +Tasks are typed using both the `Success` and `Failure`. +However, until the recent introduction of [typed throws][] to the language, +the `Failure` type could only ever have been `Never` or `any Error`. -For example, the following snippet showcases how we lose the error type information when throwing within a task: +For example, the following snippet showcases how we lose the error type +information when throwing within a task: ```swift let task = Task { @@ -51,33 +56,42 @@ Task { ``` Because the creating site has not captured a reference to the task, -this code is ignoring any failure. This *could* be the author's intention, -but it not really possible to determine this by looking the code. The community has frequently -requested this to be rectified, such that the error ignoring is +this code is ignoring any failure. +This *could* be the author's intention, but it not really possible to +determine this by looking the code. +The community has frequently requested this be rectified, +such that ignoring an error requires a more explict expression of intention. [typed throws]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md ## Proposed solution -We propose two changes to the `Task` initialization functions to address these problems: +We propose two changes to the `Task` initialization functions to address +these problems: - adopt typed throws - remove the use of `@discardableResult` unless `Failure` is `Never` ## Detailed design -`Task` currently has two initializers and matching detached variants: `.init` and `.detached`. +`Task` currently has two initializers and matching detached variants: +`.init` and `.detached`. -We propose to adjust these initializers by: +We propose to adjust these initializers in two ways. -1) Keeping the the the non-throwing overloads of those APIs with their `@discardableResult` annotation. +### Non-throwing overloads -This is because while it is common to create fire-and-forget tasks, like `Task { await doSomething() }`, -while ignoring the result of the task. +In these cases, the `@discardableResult` remains useful. +It is common to create fire-and-forget tasks that do not require access to the +result at the point of creation. ```swift -// Current signatures +Task { await doSomething() } +``` + +These signatures would be unchanged. +```swift extension Task where Failure == Never { @discardableResult public init( @@ -97,21 +111,27 @@ extension Task where Failure == Never { } ``` -2) Adjust the `throwing` variants of those APIs by: +### Throwing overloads + +In the cases of a non-`Never` error, the signatures would be adjusted by: - removing the `@discardableResult` attribute - adopting typed throws -We argue that the fact that accidentally forgetting to handle an error is more common and "risky", -than forgetting to obtain the result value of an unstructured task. If a task is created and it's -result is importand to handle, developers naturally will store and await it. However, ignoring errors -even in the simple "fire-and-forget" task case, may yield to unexpected and silent dropping of errors. +We argue that the fact that accidentally forgetting to handle an error is +more common and "risky" +than forgetting to obtain the result value of an unstructured task. +If a task is created and it's result is importand to handle, +developers naturally will store and await it. +However, ignoring errors even in the simple "fire-and-forget" task case, +may yield to unexpected and silent dropping of errors. -Therefore we argue that the discardable result behavior need only be dropped from the throwing versions of these APIs. +Therefore we argue that the discardable result behavior need only be dropped +from the throwing versions of these APIs. -```swift -// Proposed signatures +These signatures would be modified: +```swift extension Task { public init( priority: TaskPriority? = nil, @@ -150,9 +170,9 @@ This proposal is source compatible. However, it does intentionally introduce a warning into code that is ignoring errors that may be thrown by awaiting on an unstructured `Task`. -If the developer's intent was truly to ignore the task handle and potentially thrown error, -they should explicitly ignore it (which will silence the warning): - +If the developer's intent was truly to ignore the task handle and the +potentially thrown error, +they can explicitly ignore it to silence the warning. ```swift let = Task { @@ -160,7 +180,8 @@ let = Task { } ``` -The should imrprove code quality by making it more obvious when potential errors are being ignored. +The should imrprove code quality by making it more obvious when potential +errors are being ignored. ## ABI compatibility From 49f1fcc93e78133944032b284a3602ffccb24363 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Sat, 21 Sep 2024 09:45:45 -0400 Subject: [PATCH 07/15] Typos --- proposals/0NNN-unstructured-task-error-handling.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 657488aaf5..47faf31fb5 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -11,7 +11,7 @@ ## Introduction This proposal modifies the API of `Task` to adopt typed throws and makes it -more difficult to ignore thrown errors accidentially. +more difficult to ignore thrown errors accidentally. ## Motivation @@ -60,7 +60,7 @@ this code is ignoring any failure. This *could* be the author's intention, but it not really possible to determine this by looking the code. The community has frequently requested this be rectified, -such that ignoring an error requires a more explict expression of intention. +such that ignoring an error requires a more explicit expression of intention. [typed throws]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md @@ -121,7 +121,7 @@ In the cases of a non-`Never` error, the signatures would be adjusted by: We argue that the fact that accidentally forgetting to handle an error is more common and "risky" than forgetting to obtain the result value of an unstructured task. -If a task is created and it's result is importand to handle, +If a task is created and it's result is important to handle, developers naturally will store and await it. However, ignoring errors even in the simple "fire-and-forget" task case, may yield to unexpected and silent dropping of errors. @@ -175,12 +175,12 @@ potentially thrown error, they can explicitly ignore it to silence the warning. ```swift -let = Task { +let _ = Task { throw MyError.somethingBadHappened } ``` -The should imrprove code quality by making it more obvious when potential +The should improve code quality by making it more obvious when potential errors are being ignored. ## ABI compatibility @@ -200,7 +200,7 @@ could be an annoyance. However, choosing a surprising and potentially error-prone behavior as the default goes against Swift's general philosophy of safety. Changing this default feels like a much better balance, especially since -re-expressing the existing behavior involves such a familar language pattern. +re-expressing the existing behavior involves such a familiar language pattern. ## Acknowledgments From 21d7b8847cc5bb8f073300e14405e943fb625341 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:39:40 -0400 Subject: [PATCH 08/15] WIP - Expanding to include all overloads --- .../0NNN-unstructured-task-error-handling.md | 91 +++++++++++++------ 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 47faf31fb5..b093e521db 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -10,20 +10,13 @@ ## Introduction -This proposal modifies the API of `Task` to adopt typed throws and makes it +This proposal modifies the `Task` APIs to adopt typed throws and makes it more difficult to ignore thrown errors accidentally. ## Motivation -The purpose of unstructured tasks is to create a new asynchronous context in -which computation may happen. -Unlike the structured constructs (async lets and task groups), -unstructured tasks to not have to be awaited. -Their results and thrown errors are simple to discard by just not storing and -not awaiting on the created task's `.value`. - Tasks are typed using both the `Success` and `Failure`. -However, until the recent introduction of [typed throws][] to the language, +However, until the introduction of [typed throws][] to the language, the `Failure` type could only ever have been `Never` or `any Error`. For example, the following snippet showcases how we lose the error type @@ -56,11 +49,11 @@ Task { ``` Because the creating site has not captured a reference to the task, -this code is ignoring any failure. +this code is ignoring all failures. This *could* be the author's intention, but it not really possible to determine this by looking the code. -The community has frequently requested this be rectified, -such that ignoring an error requires a more explicit expression of intention. +The community has frequently requested this be addressed, +such that ignoring an error requires a more explicit expression of intent. [typed throws]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md @@ -74,14 +67,12 @@ these problems: ## Detailed design -`Task` currently has two initializers and matching detached variants: -`.init` and `.detached`. - +`Task` currently has many initializers and matching detached variants. We propose to adjust these initializers in two ways. ### Non-throwing overloads -In these cases, the `@discardableResult` remains useful. +In the case of a non-throwing overload, the `@discardableResult` remains useful. It is common to create fire-and-forget tasks that do not require access to the result at the point of creation. @@ -89,7 +80,7 @@ result at the point of creation. Task { await doSomething() } ``` -These signatures would be unchanged. +All variants of these signatures would be unchanged. ```swift extension Task where Failure == Never { @@ -118,12 +109,11 @@ In the cases of a non-`Never` error, the signatures would be adjusted by: - removing the `@discardableResult` attribute - adopting typed throws -We argue that the fact that accidentally forgetting to handle an error is -more common and "risky" +Accidentally forgetting to handle an error is both more common and "risky" than forgetting to obtain the result value of an unstructured task. -If a task is created and it's result is important to handle, +If a task is created and its result is important to handle, developers naturally will store and await it. -However, ignoring errors even in the simple "fire-and-forget" task case, +However, ignoring errors, even in the simple "fire-and-forget" task case, may yield to unexpected and silent dropping of errors. Therefore we argue that the discardable result behavior need only be dropped @@ -133,17 +123,54 @@ These signatures would be modified: ```swift extension Task { - public init( - priority: TaskPriority? = nil, - operation: sending @escaping @isolated(any) () async throws(Failure) -> Success + init( + name: String? = nil, + priority: TaskPriority? = nil, + operation: sending @escaping @isolated(any) () async throws(Failure) -> Success ) { // ... } - @_alwaysEmitIntoClient - public static func detached( - priority: TaskPriority? = nil, - operation: __owned sending @escaping @isolated(any) () async throws(Failure) -> Success + init( + name: String? = nil, + executorPreference taskExecutor: (any TaskExecutor)?, + priority: TaskPriority? = nil, + operation: sending @escaping () async throws(Failure) -> Success + ) { + // ... + } + + static func detached( + name: String? = nil, + priority: TaskPriority? = nil, + operation: sending @escaping @isolated(any) () async throws(Failure) -> Success + ) -> Task { + // ... + } + + static func detached( + name: String? = nil, + executorPreference taskExecutor: (any TaskExecutor)?, + priority: TaskPriority? = nil, + operation: sending @escaping () async throws(Failure) -> Success + ) -> Task { + // ... + } + + static func immediate( + name: String? = nil, + priority: TaskPriority? = nil, + executorPreference taskExecutor: consuming (any TaskExecutor)? = nil, + operation: sending @escaping @isolated(any) () async throws(Failure) -> Success + ) -> Task { + // ... + } + + static func immediateDetached( + name: String? = nil, + priority: TaskPriority? = nil, + executorPreference taskExecutor: consuming (any TaskExecutor)? = nil, + operation: sending @escaping @isolated(any) () async throws(Failure) -> Success ) -> Task { // ... } @@ -163,6 +190,14 @@ extension Task { } ``` +```swift +func withTaskCancellationHandler( + operation: () async throws -> T, + onCancel handler: () -> Void, + isolation: isolated (any Actor)? = #isolation +) async rethrows -> T +``` + ## Source compatibility This proposal is source compatible. From c7e4f36e489ef1de7a09bb585c487e0cd96bd570 Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:23:14 -0400 Subject: [PATCH 09/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index b093e521db..985ce0a97f 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -10,7 +10,7 @@ ## Introduction -This proposal modifies the `Task` APIs to adopt typed throws and makes it +This proposal modifies the `Task` creation APIs to adopt typed throws and makes it more difficult to ignore thrown errors accidentally. ## Motivation From 35f4ed504f655e5de0b3abefd1de2e6faccf6010 Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:23:30 -0400 Subject: [PATCH 10/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 985ce0a97f..390856be3c 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -11,7 +11,7 @@ ## Introduction This proposal modifies the `Task` creation APIs to adopt typed throws and makes it -more difficult to ignore thrown errors accidentally. +more obvious to notice when a task might be throwing errors that could previously be accidentally discarded. ## Motivation From 642050dab0b2da24fa9f40b50868d281a69aef4b Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:23:52 -0400 Subject: [PATCH 11/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 390856be3c..15aa0715af 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -38,7 +38,7 @@ Additionally, all the `Task` creation APIs are annotated with `@discardableResult`, including those that permit failure. This makes it extremely easy for the code creating the task to unintentionally ignore errors thrown in the body. -This default has proven to be surprising, error-prone, and difficult to debug. +This default has proven to be surprising and leads to accidentally missing thrown errors, like in this example: ```swift Task { From a497e9235adb51c1c52bf7969f22bdc362a9e9ad Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:24:06 -0400 Subject: [PATCH 12/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 15aa0715af..e95cdfbbd5 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -67,7 +67,7 @@ these problems: ## Detailed design -`Task` currently has many initializers and matching detached variants. +`Task` currently has many initializers and matching detached and immediate variants. We propose to adjust these initializers in two ways. ### Non-throwing overloads From 255ba5e1f43089e76b4db7c49e28d8f575cf0337 Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:24:22 -0400 Subject: [PATCH 13/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index e95cdfbbd5..30f2d10b07 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -99,6 +99,8 @@ extension Task where Failure == Never { ) -> Task { // ... } + + // ... same for: immediate, immediateDetached } ``` From bd77634dba08c8768fc4f655e6de24e5c45d5bb5 Mon Sep 17 00:00:00 2001 From: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:24:47 -0400 Subject: [PATCH 14/15] Update proposals/0NNN-unstructured-task-error-handling.md Co-authored-by: Konrad `ktoso` Malawski --- proposals/0NNN-unstructured-task-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 30f2d10b07..320ccb5731 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -4,7 +4,7 @@ * Authors: [Konrad 'ktoso' Malawski](https://github.com/ktoso), [Matt Massicotte](https://github.com/mattmassicotte) * Review Manager: TBD * Status: **Awaiting review** -* Implementation: [swiftlang/swift/pull/74110](https://github.com/swiftlang/swift/pull/74110) +* Implementation: [#84802](https://github.com/swiftlang/swift/pull/84802) * Upcoming Feature Flag: `TaskInitTypedThrows` * Review: ([pitch](https://forums.swift.org/t/pitch-non-discardable-throwing-tasks/74138)) From bd960f57170f73716a5392b5444705c18d957083 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:33:17 -0400 Subject: [PATCH 15/15] Remove flag, with* function mention --- proposals/0NNN-unstructured-task-error-handling.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/proposals/0NNN-unstructured-task-error-handling.md b/proposals/0NNN-unstructured-task-error-handling.md index 320ccb5731..6059560b0b 100644 --- a/proposals/0NNN-unstructured-task-error-handling.md +++ b/proposals/0NNN-unstructured-task-error-handling.md @@ -5,7 +5,6 @@ * Review Manager: TBD * Status: **Awaiting review** * Implementation: [#84802](https://github.com/swiftlang/swift/pull/84802) -* Upcoming Feature Flag: `TaskInitTypedThrows` * Review: ([pitch](https://forums.swift.org/t/pitch-non-discardable-throwing-tasks/74138)) ## Introduction @@ -38,7 +37,8 @@ Additionally, all the `Task` creation APIs are annotated with `@discardableResult`, including those that permit failure. This makes it extremely easy for the code creating the task to unintentionally ignore errors thrown in the body. -This default has proven to be surprising and leads to accidentally missing thrown errors, like in this example: +This default has proven to be surprising and leads to accidentally missing thrown errors, +like in this example: ```swift Task { @@ -192,14 +192,6 @@ extension Task { } ``` -```swift -func withTaskCancellationHandler( - operation: () async throws -> T, - onCancel handler: () -> Void, - isolation: isolated (any Actor)? = #isolation -) async rethrows -> T -``` - ## Source compatibility This proposal is source compatible.