Anyone have any thoughts, opinions, etc. on this? I find it kind of strange 
that I’ve received off-list feedback from within Apple, but so far it’s been 
generally ignored publicly on the list. Surely I’m not the only one who cares 
about the lack of parity between NSError and ErrorProtocol.

Charles

> On May 6, 2016, at 10:16 PM, Charles Srstka <cocoa...@charlessoft.com> wrote:
> 
>> On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution 
>> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>> 
>> I formerly posted a less-fleshed-out version of this in the “Reducing 
>> bridging magic” thread, but I thought this might warrant its own pitch. What 
>> do you all think?
>> 
>> MOTIVATION:
>> 
>> Over the past couple of years, Swift has made great strides toward seamless 
>> interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, 
>> SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better 
>> in that regard. However, there still exists one major pain point when going 
>> back and forth between Swift and Objective-C, and that lies in the area of 
>> error reporting. Passing errors between Objective-C and Swift APIs is 
>> currently quite awkward, for several reasons:
>> 
>> - The Swift-approved mechanism for reporting errors is a protocol named 
>> ErrorType (ErrorProtocol in the latest sources). However, Objective-C 
>> represent errors using a class named NSError. In addition to being a 
>> reference type, which feels quite unnatural for an error object by Swift’s 
>> conventions, NSError follows a completely paradigm from what most 
>> ErrorProtocol objects use to store errors, using a string-based domain and 
>> and integer code, along with a userInfo dictionary to store information to 
>> be presented to the user. While the domain and code are available as methods 
>> on ErrorProtocol, they are prefixed with underscores, and there is no direct 
>> equivalent to userInfo.
>> 
>> - Unlike other Objective-C classes like NSString and NSArray which are 
>> consistently bridged to value types when presenting Objective-C interfaces 
>> to Swift, the handling of NSError objects is inconsistent. Objective-C APIs 
>> which return an error by reference using an autoreleasing NSError ** pointer 
>> are converted to use the Swift try/catch mechanism, presenting the returned 
>> error as an ErrorProtocol (which is actually an NSError). Similarly, Swift 
>> APIs using try/catch are presented to Objective-C as autoreleasing NSError 
>> ** pointers, and the ErrorProtocol-conforming error is converted to an 
>> NSError when it is called by Objective-C. However, when passing around error 
>> objects in any way other than these, the errors are not bridged. An 
>> Objective-C API that takes an NSError, such as NSApp’s -presentError: 
>> method, still leaves NSError as the type in the interface presented to 
>> Swift, as do the many asynchronous APIs in Cocoa that return an NSError as 
>> one of the arguments to a completion handler. Swift APIs that accept 
>> ErrorProtocols, on the other hand, are not presented to Objective-C at all, 
>> necessitating any such APIs also be declared to take NSErrors.
>> 
>> - To convert ErrorProtocols to NSErrors, Swift provides a bridging 
>> mechanism, invoked via “as NSError”, which wraps the error in a private 
>> NSError subclass class called _SwiftNativeNSError. This subclass can be cast 
>> back to the original error type, thus returning the original wrapped error. 
>> When a Swift API that is marked “throws” is called from Objective-C and then 
>> throws an error, the same bridging mechanism is invoked. However, this 
>> bridging is not very useful, since Cocoa tends to use NSError’s userInfo 
>> dictionary to present error information to the user, and ErrorProtocol 
>> contains no equivalent to the userInfo dictionary. The result of this is 
>> that when a Swift API throws an error, and this error is passed to Cocoa, 
>> the user tends to get a generic error message instead of something actually 
>> useful.
>> 
>> - The above problem means that a Swift developer must be very careful never 
>> to use “as NSError”, and to be sure to construct an NSError when throwing an 
>> error in an API that may be called from Objective-C, rather than simply 
>> throwing the error directly, or else the error will not be properly 
>> presented. If the developer makes a mistake here, it will not be known until 
>> runtime. I have personally wasted quite a bit of time trying to hunt down 
>> points in a complicated program where an error was accidentally converted to 
>> NSError via the bridge rather than explicitly.
>> 
>> - The same problem also puts the Swift developer between a rock and a hard 
>> place, if they have other code that wants to check these errors. In a 
>> pure-Swift program, checking against a particular error can often be done 
>> simply via an equality check. If the error has been converted to NSError via 
>> the bridge, this also works, since the bridge will return the original Swift 
>> error when casted. However, if the API that threw the error has been 
>> conscientious about constructing an NSError to avoid the userInfo issue, the 
>> NSError will not be easily castable back to the original Swift error type. 
>> Instead, the developer will have to compare the NSError’s error domain and 
>> code. The code itself will have to have been assigned by the throwing API. 
>> As the domain is stringly-typed and the code will often be extraneous to the 
>> actual error definition, this is all very runtime-dependent and can easily 
>> become incorrect or out of sync, which will break the program’s error 
>> reporting.
>> 
>> - The UI for creating NSError objects is extremely verbose, and eminently 
>> un-Swift-like, usually requiring two lines of code: one to construct a 
>> dictionary, with an extremely verbose 
>> key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message 
>> text to the user, and one to construct the NSError object. The latter is 
>> itself quite verbose, requiring the developer to enter values for a domain 
>> and code which she typically does not care about, since ErrorProtocol 
>> provides decent enough default implementations for those values in most 
>> cases.
>> 
>> - Due to bugs in the bridging mechanism, it is possible for a 
>> _SwiftNativeNSError to get run a second time through the bridge, which 
>> removes the userInfo dictionary altogether, once again result in incorrect 
>> error reporting.
>> 
>> - The need for the “as NSError” bridging mechanism makes it more difficult 
>> to implement otherwise positive changes such as Joe Groff’s proposal to 
>> simplify the “as?” keyword 
>> (https://github.com/apple/swift-evolution/pull/289 
>> <https://github.com/apple/swift-evolution/pull/289>).
>> 
>> - Finally, the fact that Swift code that deals with errors must always be 
>> filled with either “as NSError” statements or explicit NSError 
>> initializations sprinkled through results in code that is quite a bit uglier 
>> than it needs to be.
>> 
>> PROPOSED APPROACH:
>> 
>> I propose consistently bridging NSError to a value type whenever it is 
>> exposed to Swift code via an API signature, and doing the equivalent in the 
>> opposite direction, similarly to how NSStrings and Strings are bridged to 
>> and from each other in API signatures.
>> 
>> The benefits of this approach are many:
>> 
>> 1. This is very similar to the bridging that already exists for 
>> String<->NSString, Array<->NSArray, when crossing the language boundary, so 
>> this improves the consistency of the language.
>> 
>> 2. Special-case type checks would be mostly restricted to the special magic 
>> that the compiler inserts when crossing the boundary, thus reducing the 
>> potential for bugs.
>> 
>> 3. NSError is no longer required to conform to ErrorProtocol, reducing the 
>> type checking that has to go on during the bridging process, also reducing 
>> the potential for bugs.
>> 
>> 4. Since the is, as, as?, and as! operators would no longer be needed to 
>> bridge NSErrors to native errors and back, improvements to that mechanism 
>> such as (https://github.com/apple/swift-evolution/pull/289 
>> <https://github.com/apple/swift-evolution/pull/289>) become viable, and the 
>> casting operators can be made to no longer act in ways that are often 
>> surprising and confusing.
>> 
>> 5. The programmer never has to deal with NSError objects in Swift code again.
>> 
>> DETAILED DESIGN:
>> 
>> 1. Extend ErrorProtocol such that it has public, non-underscored methods for 
>> the domain, code, and userInfo. The first two of these retain their existing 
>> default implementations, whereas the last of these will have a default 
>> implementation that just returns an empty dictionary. The user can override 
>> any of these to provide more information as needed.
>> 
>> 2. NSError’s conformance to ErrorProtocol is removed, since Swift code will 
>> generally no longer need to work directly with NSErrors.
>> 
>> 3. A new private error value type is introduced that conforms to 
>> ErrorProtocol. Since this type will be private, its specific name is up to 
>> the implementers, but for the purpose of this example we will assume that it 
>> is named _ObjCErrorType. This type wraps an NSError, and forwards its 
>> domain, code, and userInfo properties to it.
>> 
>> 4. The existing _SwiftNativeNSError class remains, and continues to work as 
>> it does currently, although it is extended to forward the userInfo property 
>> to the wrapped Swift error. Thus, this class now wraps a native Swift error 
>> and forwards the domain, code, and userInfo properties to it.
>> 
>> 5. Objective-C APIs that return an NSError object present it as 
>> ErrorProtocol in the signature. When called by Swift, the type of the 
>> NSError is checked. If the type is _SwiftNativeNSError, the original Swift 
>> error is unwrapped and returned. Otherwise, the NSError is wrapped in an 
>> instance of _ObjCErrorType and returned as an ErrorProtocol.
>> 
>> 6. Objective-C APIs that take NSError objects now show ErrorProtocol in 
>> their signatures as well. If an _ObjCErrorType is passed to one of these 
>> APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the 
>> error is wrapped in a _SwiftNativeNSError and passed through to the API.
>> 
>> 7. Swift errors would still be convertible to NSError, if the developer 
>> needed to do so manually. This could be done either via the current “as 
>> NSError” bridge, or via initializers and/or accessors on NSError.
>> 
>> IMPACT ON EXISTING CODE:
>> 
>> Required changes to existing code will mostly involve removing “as NSError” 
>> statements. Workarounds to the problem being addressed by this change will 
>> probably also want to be removed, as they will no longer be needed.
>> 
>> ALTERNATIVES CONSIDERED:
>> 
>> Do nothing, and let the terrorists win.
> 
> I’ve been asked, off list, to flesh out how this would affect NSErrors that 
> managed to slip in. What I am thinking is that this would be handled very 
> similarly to how other bridged Foundation value types are handled:
> 
> let stringGotThrough: NSString = …
> let errorGotThrough: NSError = …
> let userInfo: [NSObject : AnyObject] = …
> 
> let string = stringGotThrough as String
> let error = errorGotThrough as ErrorProtocol
> 
> if let failureReason = userInfo[NSLocalizedFailureReasonErrorKey] as? String {
>     print(“Failed because: \(failureReason)”)
> }
> 
> if let underlyingError = userInfo[NSUnderlyingErrorKey] as? ErrorProtocol {
>     // do something with the underlying error
> }
> 
> The obvious caveat is that since ErrorProtocol is a protocol rather than a 
> concrete type, the bridging magic we have in place probably isn’t able to 
> handle that, and would need to be extended. If I had to guess, I’d suppose 
> this is why this isn’t implemented already. However, if Joe’s bridging magic 
> reduction proposal (https://github.com/apple/swift-evolution/pull/289 
> <https://github.com/apple/swift-evolution/pull/289>) and Riley’s factory 
> initializers proposal (https://github.com/apple/swift-evolution/pull/247 
> <https://github.com/apple/swift-evolution/pull/247>), both of which I think 
> would be positive improvements to the language, are implemented, then this 
> actually gets a lot easier (and simpler) to implement, as it would all be 
> done through factory initializers, which thanks to Riley’s proposal, we’d be 
> able to put on a protocol. So in this case, we’d have:
> 
> let stringGotThrough: NSString = …
> let errorGotThrough: NSError = …
> let userInfo: [NSObject : AnyObject] = …
> 
> let string = String(stringGotThrough)
> let error = ErrorProtocol(errorGotThrough)
> 
> if let failureReason = 
> String(userInfo[NSLocalizedFailureReasonWhyIsThisNameSoDamnLongErrorKey]) {
>     print(“Failed because: \(failureReason)”)
> }
> 
> if let underlyingError = ErrorProtocol(userInfo[NSUnderlyingErrorKey]) {
>     // do something with the error
> }
> 
> The crux of it for me here is that with either method, the dictionary’s just 
> vending AnyObjects to us and thus we have to cast them anyway. Casting that 
> AnyObject to an ErrorProtocol vs. casting it to an NSError doesn’t seem 
> conceptually different at all to me, other than only needing to keep track of 
> one error paradigm instead of two wildly disparate ones.
> 
> The factory initializers (or bridging magic) would work like this:
> 
> ErrorProtocol() or “as? ErrorProtocol”: Checks if the object is a 
> _SwiftNativeNSError, and if it is, unwraps the underlying native Swift error. 
> Otherwise, it checks if we have an NSError, and if we do, it wraps it in an 
> _ObjCErrorType. If it’s not an NSError at all, this returns nil.
> 
> NSError() or “as? NSError”: Checks if the object is an _ObjCErrorType, and if 
> it is, unwraps the underlying NSError. Otherwise, it checks if we have an 
> ErrorProtocol, and if we do, it wraps it in a _SwiftNativeNSError. If it’s 
> not an ErrorProtocol at all, this returns nil.
> 
> The “Alternatives Considered” here would be to go with a public error value 
> type instead of the private _ObjCErrorType. In this case, it would probably 
> just be called something like “Error” to parallel String, Array, etc.
> 
> Charles

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to