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()