ErrorType and userInfo dictionaries

NSError has a way to carry additional information around with it: the userInfo dictionary. This includes user-facing information like the localized description, of course, but it also includes tremendously useful details such as underlying errors, particular problem files, etc.


ErrorType enums have a way to carry associated data too: values associated with the cases.


Is there some way to bridge these two worlds? Obviously the ideal thing would be if Swift synthesized code that automatically mapped associated value tuple fields into userInfo keys, but even if I had to implement a function or computed property to do it myself, that'd be way better than nothing.

El Capitan provides a new class method on NSError named +[NSError setUserInfoValueProviderForDomain:provider:] which allows you to decouple the code for constructing userInfo dictionaries from your worker code. Since you cast an ErrorType to an NSError when you send it to APIs like -[NSApp presentError:], I'd expect that this would work with ErrorTypes as well. It's pretty elegant, actually; your worker code can only have the bare minimum needed to mark an error (throw MyErrorEnum.NuclearWar), and the code to translate that into "Sorry, you have unleashed the apocalypse, and now the living envy the dead." can be somewhere else.

If you need to work in both ObjC and Swift, why not just use the NSError type and be done with it?

That will no doubt work wonderfully for things like localizedDescription, but the userInfo dictionary often contains data for programmatic consumption, too. Think of NSUnderlyingErrorKey, or the many useful ErrorKeys in Core Data. These things can't be provided by an error provider, both because they're specific to a particular instance of the error object, and because the error provider is only called if you use the methods on NSError, not if you look up keys directly in the userInfo dictionary.

Because constructing NSErrors—and, more importantly, building up error domains and codes—was tremendously awkward in Swift 1. I did it a few times, and although you could get some reasonably elegant things going if you tried, it was always a boilerplate-filled chore. If ErrorType can do what I need it to do, it's a tremendously better solution than the old way.

Hi Brent,


All ErrorType's (including enums written in pure swift) convert to NSError. You can convert them to NSError and poke at them with the standard NSError API to see what is in them.


-Chris

ErrorType is simply an empty protocol. You need to create your own types that adhere to it.


I guess I don't understand what is so awkward about it:


let ErrorDomain = "io.owensd.samples"
let info = ["name": "David", "age": "33"]
let error = NSError(domain: ErrorDomain, code: 123, userInfo: info)


Maybe I'm just misunderstanding you?

But enums with associated values do not carry that across the boundary though.


The Swift:

public enum MyError: ErrorType {
    case Code(code: Int)
    case Message(code: Int, message: String)
}
@objc public class MyType : NSObject {
    public func throwException(type: Int) throws {
        if type == 0 {
            throw MyError.Code(code: type)
        }
        else {
            throw MyError.Message(code: type, message: "Hello!")
        }
    }
}


The ObjC:

int main() {
    MyType *type = [[MyType alloc] init];
  
    NSError *error1;
    if ([type throwException:0 error:&error1] == NO) {
        NSLog(@"error: %@", error1);
    }
    NSError *error2;
    if ([type throwException:123 error:&error2] == NO) {
        NSLog(@"error: %@", error2);
    }
}


The NSError's domain is set to the module name plus type name of the error, and the code is set to the ordinal offset of the enum value. However, the code and message data is lost. Is that just a bug?

It seems like it would be trivial for them to add the ability to extend a type that conforms to ErrorType to send a userInfo dictionary in that direction, considering that the domain and code are already created by a default implementation in an extension of ErrorType.


The domain and code used when converting to an NSError can be customized by implementing the (not-for-production-code) properties _domain and _code.


enum MyError: ErrorType
{
    case Simple(num: Int)
    case Verbose(num: Int, message: String)
  
    var _domain: String {return "testing_MyError_domain"}
    var _code: Int
        {
            switch self
            {
            case .Simple(let num): return num
            case .Verbose(let num, _): return num
            }
    }
}

do {throw MyError.Verbose(num: 404, message: "Hello?")}
catch
{
    var str = (error as NSError).description
    let info = (error as NSError).userInfo
}


There may be an issue of where this stuff could be publically declared/exposed though, since it probably belongs in Cocoa rather than in the Swift standard library.

How about adding a function to ErrorType enum that would construct an NSError with an underlying error?


enum DocumentError: ErrorType {

    case CantSave

    func withUnderlying(error: ErrorType?) -> NSError {
        guard let error = error else {
            return self as NSError
        }
        let info = [NSUnderlyingErrorKey: error as NSError]
        return NSError(domain: _domain, code: _code, userInfo: info)
    }
}

let cocoaError = NSError(domain: NSCocoaErrorDomain, code: NSFileWriteFileExistsError, userInfo: nil)
let error = DocumentError.CantSave.withUnderlying(cocoaError)
ErrorType and userInfo dictionaries
 
 
Q