Converting ErrorType to NSError loses related objects

In Swift 2.0, NSError follows the ErrorType protocol.

For a specially defined error, we can specify an associative object for some cases, for example 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 move to other places like NSError , it loses information about the associated objects.

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

prints:

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

and his userInfo is nil .

Where is my wife related to ErrorType ?

+52
swift swift2 error-handling
Jul 15 '15 at 5:16
source share
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] } } } 
+34
Aug 01 '16 at 23:23
source share

An ErrorType cannot be passed to NSError , you need to take the associated data and put it in NSError yourself.

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

EDIT: in fact, you can do acting from ErrorType to NSError , but the NSError that you get from the default implementation is pretty primitive. What I do in my application is to connect the application: willPresentError: in my delegate delet and use the special class to read my ErrorType application and decorate NSErrors to return.

+23
Jul 28 '15 at 19:30
source share

Creating an NSError in each catch block can lead to a large number of copies and ErrorType to convert custom ErrorType to NSError . I distracted him like @powertoold .

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

This extension may contain code common to LifeError that we already have, and other custom error types that we can create.

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

Disconnect to 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 here 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) } 

Can you easily move certain functions like func userInfo() -> Dictionary<String,String>? , from LifeError to extension CustomErrorConvertible or another extension.

Instead of hard coding, error codes may be preferred, for example, the above enumerations.

 enum LifeError:Int { case Born case LostJob } 
+14
Oct 23 '15 at 17:10
source share

My solution to this problem was to create an enumeration that matches Int, ErrorType:

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

And then expand the enumeration to match CustomStringConvertible and CustomErrorConvertible protocol:

 extension AppError: CustomStringConvertible, CustomErrorConvertible protocol CustomErrorConvertible { var error: NSError { get } } 

For description and error, I turned on 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 wrote my own NSError:

 return NSError(domain:NSBundle.mainBundle().bundleIdentifier!, code:errorCode, userInfo:[NSLocalizedDescriptionKey: errorDescription]) 
+6
04 Oct '15 at 17:48
source share

I have a problem using PromiseKit too, and I found a workaround that may be a little ugly, but seems to work.

I insert my playground here 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() 
+2
Oct 13 '15 at 16:08
source share

The best solution I found is to have an Objective-C wrapper to throw ErrorType to NSError (via NSObject* parmeter) and retrieve userInfo . Most likely, this will work for other related objects as well.

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

Here is the Objective-C helper. Put it, for example, in the class MyErrorUtils affected by Swift:

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

Then use the assistant in Swift as follows:

 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 am currently using Xcode 8 and Swift 2.3)

+1
Oct 28 '16 at 12:48
source share

As the accepted answer noted, there is now a CustomNSError in Swift 3, however you don't have to use it. If you define your type of error like this

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

Then this error can be directly passed to NSError :

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

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

+1
May 30 '17 at 18:01
source share



All Articles