[swift-evolution] [Pitch] Consistent bridging for NSErrors at the language boundary
Charles Srstka
cocoadev at charlessoft.com
Sat May 14 02:31:17 CDT 2016
> On May 14, 2016, at 1:45 AM, Jon Shier via swift-evolution <swift-evolution at swift.org> wrote:
>
> Charles:
> Foundation error reporting is based on NSError, nothing else. In none of my apps have I used my own NSError’s and in the few frameworks I use or maintain, few use NSError, and then only to avoid having to declare a custom error type, to make the API perhaps a bit more familiar to consumers.
All of the error-handling mechanisms in Cocoa are NSError-based. -[NSApplication presentError:], -[NSDocument presentError:], and the like all take NSErrors.
> All your concern about conversion to and from NSErrors is largely irrelevant to anyone who isn’t using Swift from Objective-C on a regular basis. It’s utterly irrelevant to the native Swift programmer. I’m not sure where your issue about presenting non-NSError’s come from, since the mechanisms to present an error would have to be custom written no matter the error type.
Anyone who writes Cocoa apps on OS X or iOS using Swift is most certainly using Swift from Objective-C on a regular basis, because all of the underlying frameworks are Objective-C. For example, suppose you have a document-based application:
class MyDocument: NSDocument {
enum Error: ErrorType {
case MadeAMistake
case RanOutOfCake
}
…
readFromData(data: NSData, ofType: String) {
…
if someFailureCondition {
throw Error.RanOutOfCake
}
...
}
...
}
The readFromData method above will be called by Objective-C, since the internal NSDocument mechanism will be the one calling it. Since it will have no idea how to represent that, the error dialog box that appears will simply say something unhelpful like “MyApp.MyDocument.Error error 1”. Now, there are two ways to solve this, both of which involve throwing an NSError rather than a custom error type:
Solution 1: Get rid of the ErrorType.
class MyDocument: NSDocument {
let ErrorDomain = “MyApp.MyDocument”
enum ErrorCode: Int {
case MadeAMistake = 1
case RanOutOfCake = 2
}
…
readFromData(data: NSData, ofType: String) {
…
if someFailureCondition {
let userInfo = [NSLocalizedFailureReasonErrorKey: NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)]
throw NSError(domain: ErrorDomain, code: ErrorCode.RanOutOfCake.rawValue, userInfo: userInfo)
}
...
}
...
}
This has a couple of problems. First, it forces us to use NSError. Second, it’s ugly. Imagine a method with many possible failure points, all filled with the userInfo building above.
The second solution is a little better:
class MyDocument: NSDocument {
enum Error: ErrorType {
let ErrorDomain = “MyApp.MyDocument”
case MadeAMistake
case RanOutOfCake
func toNSError() -> NSError {
let failureReason: String
let code: Int
switch self {
case .MadeAMistake:
failureReason = NSLocalizedString(“Looks like we made a mistake.”, comment: “Made a mistake”)
code = 1
case .RanOutOfCake:
failureReason = NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)
code = 2
}
let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
return NSError(domain: self.ErrorDomain, code: code, userInfo: userInfo)
}
}
…
readFromData(data: NSData, ofType: String) {
…
if someFailureCondition {
throw Error.RanOutOfCake.toNSError()
}
...
}
...
}
The good news is that now the ugliness is removed from the actual program code and confined to the error type’s declaration. The bad news is that if we forget to put .toNSError() on an error we throw somewhere and that bubbles back to Cocoa, the user gets a meaningless error message. Furthermore, if we call through to some other error-throwing method, we have to catch and convert any errors it might throw:
readFromData(data: NSData, ofType: String) {
…
do {
try somethingThatThrows()
} catch {
if let myError = error as? Error {
throw myError.toNSError()
} else if let someOtherError = error as? SomeOtherErrorType {
// convert to NSError somehow
} else if let yetAnotherError = …
etc. etc. etc.
} else {
throw error
}
}
…
}
At this point it’s probably just to use NSError all the way through. :-/
With my proposal, all you’d do is this:
class MyDocument: NSDocument {
enum Error: ErrorType {
case MadeAMistake
case RanOutOfCake
var userInfo: [NSObject : AnyObject] {
let failureReason: String
switch self {
case .MadeAMistake:
failureReason = NSLocalizedString(“Looks like we made a mistake.”, comment: “Made a mistake”)
case .RanOutOfCake:
failureReason = NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)
}
return [NSLocalizedFailureReasonErrorKey: failureReason]
}
}
…
readFromData(data: NSData, ofType: String) {
…
if someFailureCondition {
throw Error.RanOutOfCake
}
...
}
...
}
Much simpler, much, much less failure prone, and cleaner too since we don’t have to worry about domains or codes anymore, as we can just use the default values for those now.
> The drawbacks I see with exposing NSError’s properties on every ErrorType is that those properties are only there as a hack to interoperate with NSError. Really they’d go away as Swift evolves it’s own default error representation, or language leaves it to developer to build their own.
> Essentially, I think that if you want to improve ErrorType, formalizing its relationship to NSError is the wrong way. NSErrors are not good error representations, especially in Swift, and we should move away from them as soon as possible.
I don’t think Cocoa is going away for a very, very long time. Even if it did, you’d still need some consistent way to turn an ErrorType into something human-readable, and currently we don’t have that. NSError’s userInfo field actually does a fairly decent job, aside from the absurdly long constant names.
Charles
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160514/c15ab948/attachment.html>
More information about the swift-evolution
mailing list