The current withDeadline and withCancellation proposal works well for operations that throw errors on failure or cancellation. However, itās unclear how this would behave with non-throwing operations that may return partial results, such as an empty array, nil, or incomplete data upon cancellation. This is an important scenario that needs further consideration, as many async operations, such as network requests or database queries, might be designed to return partial data or default values rather than throwing errors when they are canceled.
To address this, I propose adding overloads that accommodate non-throwing operations, allowing developers to handle cases where the operation may return partial results on cancellation, or where the result is inherently nullable (e.g., an empty array or nil).
Proposed Solution
Here are two potential solutions that could fit the needs of non-throwing operations:
1. Returning a value with a cancellation error:
For non-throwing operations that return values like arrays, an overload could return a tuple containing both the result and a cancellationError field. This way, developers can check if the operation was canceled and still receive the partial result.
Example:
/// Return array with partial result if canceled
func searchItemsArray(query: String) async -> [String]
/// Usage:
func withCancellation<Return>(onDeadline: Clock.Instant, body: () async -> Return)
-> (output: Return, cancellationError: CancellationError?)
let result = withCancellation(onDeadline: deadline) {
searchItemsArray(query: "query")
}
if let error = result.cancellationError {
print("Operation was cancelled with partial result:", result.output)
} else {
print("Operation was fully completed with result:", result.output)
}
In this approach:
- If the operation completes successfully, it returns the result.
- If the operation is canceled, the function will still return the result (which may be partial or default, depending on the operation) along with a
cancellationError indicating that the operation was canceled before completion.
2. Explicit InfallibleResult Type:
For cases where you want to clearly distinguish between a fully completed operation and a canceled operation, we could use a result type such as InfallibleResult. This enum allows the operation to return either a completed result or a partial result if the operation was canceled.
This solution also work better with operations that can return Optional / nil, so I find it more general.
Example:
enum InfallibleResult<T> {
case completed(T)
case partial(T) // Returned when the operation was cancelled
}
func searchItems(query: String) async -> InfallibleResult<[String]>
/// Usage:
func withCancellation<Return>(onDeadline: Clock.Instant, body: () async -> Return)
-> InfallibleResult<Return>
let result = withCancellation(onDeadline: deadline) {
searchItems(query: "query")
}
switch result {
case .completed(let output):
print("Operation was fully completed with result:", output)
case .partial(let output):
print("Operation was cancelled with partial result:", output)
}
In this approach:
- The result type explicitly tells the developer whether the operation was completed or canceled with a partial result.
- The
partial case provides a way to handle the canceled operation without losing the data that may have been fetched before cancellation.
- returning
InfallibleResult explicitly and statically indicates that operation has proper cancellation support
Motivation
This suggestion aims to cover more use cases by handling non-throwing operations that return partial or empty results when canceled. Many async operations, such as networking or data transformations, may return partial data or default values when interrupted, and itās important for developers to have a way to clearly identify these scenarios. The overloads outlined above provide an intuitive way to distinguish between completed and partially completed (canceled) results, enabling better error handling and data management in asynchronous workflows.
These overloads provide more flexibility to handle cancellation in a way that matches operations, without needing to adopt error-prone or verbose manual timeout implementations.
PS: I can create a separate pitch if someone is interested in what I'm suggesting but feels it is out of scope of current proposal