[swift-evolution] [PITCH] Improved error handling for async Cocoa methods
Chris Lattner
clattner at apple.com
Thu Jul 14 23:54:25 CDT 2016
When/if we get an async/await like feature, of course we’ll try to pull completion handlers automatically into the model. We can do that post swift 3.
-Chris
> On Jul 14, 2016, at 3:30 PM, Charles Srstka via swift-evolution <swift-evolution at swift.org> wrote:
>
> Right, but since this would affect the Obj-C importer and thus would be a source-breaking change, it would probably not be possible anymore after Swift 3.
>
> Charles
>
>> On Jul 14, 2016, at 4:57 PM, Dan Stenmark <daniel.j.stenmark at gmail.com> wrote:
>>
>> I’d say it’s a little premature to be talking about this; the team has made it very clear that the discussion on Native Concurrency in Swift won’t begin for another couple months.
>>
>> Dan
>>
>>> On Jul 14, 2016, at 10:54 AM, Charles Srstka via swift-evolution <swift-evolution at swift.org> wrote:
>>>
>>> I know it’s late, but I was wondering what the community thought of this:
>>>
>>> MOTIVATION:
>>>
>>> With the acceptance of SE-0112, the error handling picture looks much stronger for Swift 3, but there is still one area of awkwardness remaining, in the area of returns from asynchronous methods. Specifically, many asynchronous APIs in the Cocoa framework are declared like this:
>>>
>>> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * _Nullable, NSError * _Nullable))completionHandler;
>>>
>>> This will get imported into Swift as something like this:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Bar?, Error?) -> ())
>>>
>>> The intention of this API is that either the operation will succeed, and something will be passed in the Bar parameter, and the error will be nil, or else the operation will fail, and then the error parameter will be populated while the Bar parameter is nil. However, this intention is not expressed in the API, since the syntax leaves the possibility that both parameters could be nil, or that they could both be non-nil. This forces the developer to do needless and repetitive checks against a case which in practice shouldn’t occur, as below:
>>>
>>> doSomething(foo: foo) { bar, error in
>>> if let bar = bar {
>>> // handle success case
>>> } else if let error = error {
>>> self.handleError(error)
>>> } else {
>>> self.handleError(NSCocoaError.FileReadUnknownError)
>>> }
>>> }
>>>
>>> This results in the dreaded “untested code.”
>>>
>>> Note that while it is possible that the developer could simply force-unwrap error in the failure case, this leaves the programs open to crashes in the case where a misbehaved API forgets to populate the error on failure, whereas some kind of default error would be more appropriate. The do/try/catch mechanism works around this by returning a generic _NilError in cases where this occurs.
>>>
>>> PROPOSED SOLUTION:
>>>
>>> Since the pattern for an async API that returns an error in the Cocoa APIs is very similar to the pattern for a synchronous one, we can handle it in a very similar way. To do this, we introduce a new Result enum type. We then bridge asynchronous Cocoa APIs to return this Result type instead of optional values. This more clearly expresses to the user the intent of the API.
>>>
>>> In addition to clarifying many Cocoa interfaces, this will provide a standard format for asynchronous APIs that return errors, opening the way for these APIs to be seamlessly integrated into future asynchronous features added to Swift 4 and beyond, in a way that could seamlessly interact with the do/try/catch feature as well.
>>>
>>> DETAILED DESIGN:
>>>
>>> 1. We introduce a Result type, which looks like this:
>>>
>>> enum Result<T> {
>>> case success(T)
>>> case error(Error)
>>> }
>>>
>>> 2. Methods that return one parameter asynchronously with an error are bridged like this:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Result<Bar>) -> ())
>>>
>>> and are used like this:
>>>
>>> doSomething(foo: foo) { result in
>>> switch result {
>>> case let .success(bar):
>>> // handle success
>>> case let .error(error):
>>> self.handleError(error)
>>> }
>>> }
>>>
>>> 3. Methods that return multiple parameters asynchronously with an error are bridged using a tuple:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz)>) -> ())
>>>
>>> and are used like this:
>>>
>>> doSomething(foo: foo) { result in
>>> switch result {
>>> case let .success(bar, baz):
>>> // handle success
>>> case let .error(error):
>>> self.handleError(error)
>>> }
>>> }
>>>
>>> 4. Methods that return only an error and nothing else are bridged as they are currently, with the exception of bridging NSError to Error as in SE-0112:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Error?) -> ())
>>>
>>> and are used as they currently are:
>>>
>>> doSomething(foo: foo) { error in
>>> if let error = error {
>>> // handle error
>>> } else {
>>> // handle success
>>> }
>>> }
>>>
>>> 5. For the case in part 2, the bridge works much like the do/try/catch mechanism. If the first parameter is non-nil, it is returned inside the .success case. If it is nil, then the error is returned inside the .error case if it is non-nil, and otherwise _NilError is returned in the .error case.
>>>
>>> 6. For the case in part 3, in which there are multiple return values, the same pattern is followed, with the exception that we introduce a new Objective-C annotation. I am provisionally naming this annotation NS_REQUIRED_RETURN_VALUE, but the developer team can of course rename this annotation to whatever they find appropriate. All parameters annotated with NS_REQUIRED RETURN_VALUE will be required to be non-nil in order to avoid triggering the error case. Parameters not annotated with NS_REQUIRED RETURN_VALUE will be inserted into the tuple as optionals. If there are no parameters annotated with NS_REQUIRED RETURN_VALUE, the first parameter will be implicitly annotated as such. This allows asynchronous APIs to continue to return optional secondary values if needed.
>>>
>>> Thus, the following API:
>>>
>>> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * _Nullable NS_REQUIRED_RETURN_VALUE, Baz * _Nullable NS_REQUIRED_RETURN_VALUE, NSError * _Nullable))completionHandler;
>>>
>>> is bridged as:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz)>) -> ())
>>>
>>> returning .success only if both the Bar and Baz parameters are non-nil, whereas this API:
>>>
>>> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * _Nullable NS_REQUIRED_RETURN_VALUE, Baz * _Nullable, NSError * _Nullable))completionHandler;
>>>
>>> is bridged as:
>>>
>>> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz?)>) -> ())
>>>
>>> returning .success whenever the Bar parameter is nil. An API containing no parameter annotated with NS_REQUIRED_RETURN_VALUE will be bridged the same as above.
>>>
>>> FUTURE DIRECTIONS:
>>>
>>> In the future, an asynchronous API returning a Result could be bridged to an async function, should those be added in the future, using the semantics of the do/try/catch mechanism. The bridging would be additive, similarly to how Objective-C properties declared via manually written accessor methods can nonetheless be accessed via the dot syntax. Thus,
>>>
>>> func doSomething(_ completionHandler: (Result<Foo>) -> ())
>>>
>>> could be used as if it were declared like this:
>>>
>>> async func doSomething() throws -> Foo
>>>
>>> and could be used like so:
>>>
>>> async func doSomethingBigger() {
>>> do {
>>> let foo = try await doSomething()
>>>
>>> // do something with foo
>>> } catch {
>>> // handle the error
>>> }
>>> }
>>>
>>> making asynchronous APIs convenient to write indeed.
>>>
>>> ALTERNATIVES CONSIDERED:
>>>
>>> Leaving the somewhat ambiguous situation as is.
>>>
>>> Charles
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
More information about the swift-evolution
mailing list