[swift-evolution] [PITCH] Improved error handling for async Cocoa methods
Dan Appel
dan.appel00 at gmail.com
Thu Jul 14 13:30:18 CDT 2016
Result is one way to do it, but I'm not a huge fan. I would personally
prefer this kind of api.
doSomethingAsync { getResult in
do {
let result = try getResult()
} catch {
// handle error
}
}
Where the signature would go from
func doSomethingAsync(callback: (T?, Error?) -> ())
to
func doSomethingAsync(callback: (() throws -> T) -> ())
No new types defined; the current callbacks just have to be wrapped in
another closure.
On Thu, 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
>
--
Dan Appel
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160714/46c5a23d/attachment.html>
More information about the swift-evolution
mailing list