Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting ErrorType to NSError loses associated objects

In Swift 2.0 NSError conforms to the ErrorType protocol.

For a customly defined error, we can specify the associating object(s) for some cases, like below.

enum LifeError: ErrorType {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByWife(wife: String)
    ...
}

We can comfortably do the following:

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    ...
}

However if we want it to pass into other places as an NSError, it loses its associating object information.

println("\(LifeError.GetCaughtByWife("Name") as NSError)")

prints:

Error Domain=... Code=1 "The operation couldn't be completed". (... error 1)

and its userInfo is nil.

Where is my wife associated with the ErrorType?

like image 761
Ben Lu Avatar asked Jul 15 '15 05:07

Ben Lu


7 Answers

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]
        }
    }
}
like image 88
Igor Camilo Avatar answered Sep 22 '22 00:09

Igor Camilo


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.

like image 43
iluvcapra Avatar answered Sep 25 '22 00:09

iluvcapra


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
}
like image 20
orkoden Avatar answered Sep 23 '22 00:09

orkoden


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])
like image 34
powertoold Avatar answered Sep 23 '22 00:09

powertoold


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()
like image 43
Charly Liu Avatar answered Sep 21 '22 00:09

Charly Liu


The best solution that I found, is to have an Objective-C wrapper for casting the ErrorType to NSError (via NSObject* parmeter) and extracting the userInfo. Most likely this would work for other associated objects too.

In my case all other attempts using only Swift resulted in getting a nil userInfo.

Here is the Objective-C helper. Place it for example in a MyErrorUtils class exposed to Swift:

+ (NSDictionary*)getUserInfo:(NSObject *)error {
    NSError *nsError = (NSError *)error;
    if (nsError != nil) {
        return [nsError userInfo];
    } else {
        return nil;
    }
}

Then use the helper in Swift like this:

static func myErrorHandler(error: ErrorType) {

    // Note the as? cast to NSObject
    if let userInfo: [NSObject: AnyObject]? = 
        MyErrorUtils.getUserInfo(error as? NSObject) {

        let myUserInfo = userInfo["myCustomUserInfo"]

        // ... Error processing based on userInfo ...
    }

}

(I'm currently using XCode 8 and Swift 2.3)

like image 23
Peter Lamberg Avatar answered Sep 23 '22 00:09

Peter Lamberg


As the accepted answer pointed out, there's now CustomNSError in Swift 3, however, you don't necessarily need to use it. If you define your error type like this

@objc
enum MyErrorType: Int, Error { ... }

Then this error can directly be casted to NSError:

let error: MyErrorType = ...
let objcError = error as NSError

I just discovered that today and though I share it with the world.

like image 21
Mecki Avatar answered Sep 22 '22 00:09

Mecki