Converting ErrorType to NSError loses associated objects

Solution 1:

New in Xcode 8: CustomNSError protocol.

enum LifeError: CustomNSError {
    case beBorn
    case lostJob(job: String)
    case getCaughtByWife(wife: String)

    static var errorDomain: String {
        return "LifeError"
    }

    var errorCode: Int {
        switch self {
        case .beBorn:
            return 0
        case .lostJob(_):
            return 1
        case .getCaughtByWife(_):
            return 2
        }
    }

    var errorUserInfo: [String : AnyObject] {
        switch self {
        case .beBorn:
            return [:]
        case .lostJob(let job):
            return ["Job": job]
        case .getCaughtByWife(let wife):
            return ["Wife": wife]
        }
    }
}

Solution 2:

An ErrorType can't really be casted to an NSError, you have to take the associated data and package it into an NSError yourself.

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    throw NSError(domain:LifeErrorDomain code:-1 userInfo:
        [NSLocalizedDescriptionKey:"You cheated on \(wife)")
}

EDIT: Actually you can do the cast from ErrorType to NSError, but the NSError you get from the default implementation is quite primitive. What I'm doing in my app is hooking application:willPresentError: in my app delegate and using a custom class to read the my app's ErrorType's and decorate NSErrors to return.

Solution 3:

Creating an NSError in every catch block can lead to a lot of copy and paste to convert your custom ErrorType to NSError. I abstracted it away similar to @powertoold.

protocol CustomErrorConvertible {
    func userInfo() -> Dictionary<String,String>?
    func errorDomain() -> String
    func errorCode() -> Int
}

This extension can hold code, that's common for the LifeError we already have and other custom error types we may create.

extension CustomErrorConvertible {
    func error() -> NSError {
        return NSError(domain: self.errorDomain(), code: self.errorCode(), userInfo: self.userInfo())
    }
}

Off to the implementation!

enum LifeError: ErrorType, CustomErrorConvertible {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByPolice(police: String)

    func errorDomain() -> String {
        return "LifeErrorDomain"
    }

    func userInfo() -> Dictionary<String,String>? {
        var userInfo:Dictionary<String,String>?
        if let errorString = errorDescription() {
            userInfo = [NSLocalizedDescriptionKey: errorString]
        }
        return userInfo
    }

    func errorDescription() -> String? {
        var errorString:String?
        switch self {
        case .LostJob(let job):
            errorString = "fired as " + job
        case .GetCaughtByPolice(let cops):
            errorString = "arrested by " + cops
        default:
            break;
        }
        return errorString
    }

    func errorCode() -> Int {
        switch self {
        case .BeBorn:
            return 1
        case .LostJob(_):
            return -9000
        case .GetCaughtByPolice(_):
            return 50
        }
    }
}

And this is how to use it.

func lifeErrorThrow() throws {
    throw LifeError.LostJob(job: "L33tHax0r")
}

do {
    try lifeErrorThrow()
}
catch LifeError.BeBorn {
  print("vala morgulis")
}
catch let myerr as LifeError {
    let error = myerr.error()
    print(error)
}

You could easily move certain functions like func userInfo() -> Dictionary<String,String>? from LifeError to extension CustomErrorConvertible or a different extension.

Instead of hardcoding the error codes like above an enum might be preferable.

enum LifeError:Int {
  case Born
  case LostJob
}

Solution 4:

My solution to this problem was to create an enum that conforms to Int, ErrorType:

enum AppError: Int, ErrorType {
    case UserNotLoggedIn
    case InternetUnavailable
}

And then extend the enum to conform to CustomStringConvertible and a custom protocol called CustomErrorConvertible:

extension AppError: CustomStringConvertible, CustomErrorConvertible

protocol CustomErrorConvertible {
    var error: NSError { get }
}

For the description and error, I switched on the AppError. Example:

Description:    switch self {
            case .UserNotLoggedIn: return NSLocalizedString("ErrorUserNotLoggedIn", comment: "User not logged into cloud account.")
            case .InternetUnavailable: return NSLocalizedString("ErrorInternetUnavailable", comment: "Internet connection not available.")
            }

Error:    switch self {
            case .UserNotLoggedIn: errorCode = UserNotLoggedIn.rawValue; errorDescription = UserNotLoggedIn.description
            case .InternetUnavailable: errorCode = InternetUnavailable.rawValue; errorDescription = InternetUnavailable.description
            }

And then I composed my own NSError:

return NSError(domain:NSBundle.mainBundle().bundleIdentifier!, code:errorCode, userInfo:[NSLocalizedDescriptionKey: errorDescription])

Solution 5:

I'm having this problem too using PromiseKit and I found a workaround that may be a bit ugly but seems to work.

I paste here my playground so you can see the whole process.

import Foundation
import PromiseKit
import XCPlayground

let error = NSError(domain: "a", code: 1, userInfo: ["hello":"hello"])

// Only casting won't lose the user info

let castedError = error as ErrorType
let stillHaveUserInfo = castedError as NSError

// when using promises

func convert(error: ErrorType) -> Promise<Int> {
    return Promise<Int> {
        (fulfill, reject) in
        reject(error)
    }
}

let promiseA = convert(error)

// Seems to lose the user info once we cast back to NSError

promiseA.report { (promiseError) -> Void in
    let lostUserInfo = promiseError as NSError
}


// Workaround

protocol CastingNSErrorHelper {
    var userInfo: [NSObject : AnyObject] { get }
}

extension NSError : CastingNSErrorHelper {}

promiseA.report { (promiseError) -> Void in
    let castingNSErrorHelper = promiseError as! CastingNSErrorHelper
    let recoveredErrorWithUserInfo = castingNSErrorHelper as! NSError
}

XCPSetExecutionShouldContinueIndefinitely()