This question already has answers here:
How to provide a localized description with an Error type in Swift?
(7 answers)
Closed 6 years ago.
I've an error class that is:
public enum ModelError: Error {
case invalidArray(model: String)
var localizedDescription: String {
switch self {
case .invalidArray(model: let model):
return "\(model) has an invalid array"
default:
return "modelError"
}
}
}
and when passed as an Error in a callback function, I want to access its custom localizedDescription. For instance:
func report(_ error: Error) {
print("Error report: \(error.localizedDescription)")
}
But calling report(ModelError.invalidArray(model: "test")) prints:
"The operation couldn’t be completed. (ModelError error 0.)"
Such things seems feasible with NSError since I can override the localizedDescription property there. But I don't want to use NSError since it's not really a swift thing and a lot of libraries work with Error.
According to the Documentation, localizedDescription is implemented in a protocol extension, not in the protocol declaration, which means there's nothing to adhere to or override. There is a type-wide interface for enums that adhere to Error.
My way I get around this is to use a wrapper protocol:
protocol LocalizedDescriptionError: Error {
var localizedDescription: String { get }
}
public enum ModelError: LocalizedDescriptionError {
case invalidArray(model: String)
var localizedDescription: String {
switch self {
case .invalidArray(model: let model):
return "\(model) has an invalid array"
default:
return "modelError"
}
}
}
let error: LocalizedDescriptionError = ModelError.invalidArray(model: "Model")
let text = error.localizedDescription // Model Has an invalid array
Related
A lot of times, I'll receive a Swift Error object from a framework, which is really an NSError.
In order to access its information (e.g. code), I need to cast it to an NSError:
(error as NSError).code == ....
Why is this just an unconditional as? If I design my own error class that conforms to Error, it won't necessarily be an NSError, so how can this be the correct way to perform this cast?
Is there some kind of special case in the type system? This is a downcast that behaves like an upcast.
I believe the capability for Error to be convertible to NSError is hardcoded into the compiler, and the actual bridging is implemented in the Swift runtime.
In runtime/ErrorObject.mm, I found this comment:
// This implements the object representation of the standard Error
// type, which represents recoverable errors in the language. This
// implementation is designed to interoperate efficiently with Cocoa libraries
// by:
// - ...
// - allowing a native Swift error to lazily "become" an NSError when
// passed into Cocoa, allowing for cheap Swift to Cocoa interop
And this function:
/// Take an Error box and turn it into a valid NSError instance.
id
swift::_swift_stdlib_bridgeErrorToNSError(SwiftError *errorObject) {
...
// Otherwise, calculate the domain, code, and user info, and
// initialize the NSError.
auto value = SwiftError::getIndirectValue(&errorObject);
auto type = errorObject->getType();
auto witness = errorObject->getErrorConformance();
NSString *domain = getErrorDomainNSString(value, type, witness);
NSInteger code = getErrorCode(value, type, witness);
NSDictionary *userInfo = getErrorUserInfoNSDictionary(value, type, witness);
...
}
The ErrorHandling.rst document says this about the rationale:
It should be possible to turn an arbitrary Swift enum that conforms to Error into an NSError by using the qualified type name as the domain key, the enumerator as the error code, and turning the payload into user data.
(Parts of the document may be outdated.)
And this is (I think) at least one part in the type checker were the information that Error is convertible to NSError is encoded (there are probably more):
// Check whether the type is an existential that contains
// Error. If so, it's bridged to NSError.
if (type->isExistentialWithError()) {
if (auto nsErrorDecl = getNSErrorDecl()) {
// The corresponding value type is Error.
if (bridgedValueType)
*bridgedValueType = getErrorDecl()->getDeclaredInterfaceType();
return nsErrorDecl->getDeclaredInterfaceType();
}
}
This is a great question.
I thought I saw "An Error type can be bridged to an NSError" somewhere, but that must have been Xcode or some tutorial online.
Luckily I found this from swift/NSError.swift.
// NSError and CFError conform to the standard Error protocol. Compiler
// magic allows this to be done as a "toll-free" conversion when an NSError
// or CFError is used as an Error existential.
extension NSError : Error {
#nonobjc
public var _domain: String { return domain }
#nonobjc
public var _code: Int { return code }
#nonobjc
public var _userInfo: AnyObject? { return userInfo as NSDictionary }
/// The "embedded" NSError is itself.
#nonobjc
public func _getEmbeddedNSError() -> AnyObject? {
return self
}
}
extension CFError : Error {
public var _domain: String {
return CFErrorGetDomain(self) as String
}
public var _code: Int {
return CFErrorGetCode(self)
}
public var _userInfo: AnyObject? {
return CFErrorCopyUserInfo(self) as AnyObject
}
/// The "embedded" NSError is itself.
public func _getEmbeddedNSError() -> AnyObject? {
return self
}
}
switch error {
case _ where (error as NSError).domain == self.domain:
print("error from particular domain")
default:
print("default error")
}
I'm writing an error logger using Crashlytics and I've come up against an issue that is making me question my understanding of protocols and dynamic dispatch.
When recording non fatal errors using Crashlytics the API expects an Error conforming object, and an optional user info dictionary. I'm looking at JSON decoding errors at the moment, and I wasn't too happy with what I was seeing in the Crashlytics dashboard when I just sent the DecodingError along in recordError. So my solution was to write an extension for DecodingError adopting CustomNSError to provide some more verbose info to help with debugging in the future:
extension DecodingError: CustomNSError {
public static var errorDomain: String {
return "com.domain.App.ErrorDomain.DecodingError"
}
public var errorCode: Int {
switch self {
case .dataCorrupted:
return 1
case .keyNotFound:
return 2
case .typeMismatch:
return 3
case .valueNotFound:
return 4
}
}
public var errorUserInfo: [String : Any] {
switch self {
case .dataCorrupted(let context):
var userInfo: [String: Any] = [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: ".")
]
guard let underlyingError = context.underlyingError else { return userInfo }
userInfo["underlyingErrorLocalizedDescription"] = underlyingError.localizedDescription
userInfo["underlyingErrorDebugDescription"] = (underlyingError as NSError).debugDescription
userInfo["underlyingErrorUserInfo"] = (underlyingError as NSError).userInfo.map {
return "\($0.key): \(String(describing: $0.value))"
}.joined(separator: ", ")
return userInfo
case .keyNotFound(let codingKey, let context):
return [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: "."),
"codingKey": codingKey.stringValue
]
case .typeMismatch(_, let context), .valueNotFound(_, let context):
return [
"debugDescription": context.debugDescription,
"codingPath": context.codingPath.map { $0.stringValue }.joined(separator: ".")
]
}
}
}
I've written a method in my logger which looks like this:
func log(_ error: CustomNSError) {
Crashlytics.sharedInstance().recordError(error)
}
And I send the error along here:
do {
let decoder = JSONDecoder()
let test = try decoder.decode(SomeObject.self, from: someShitJSON)
} catch(let error as DecodingError) {
switch error {
case .dataCorrupted(let context):
ErrorLogger.sharedInstance.log(error)
default:
break
}
}
But the object that gets passed to the log(_ error:) is not my implementation of CustomNSError, looks like a standard NSError with the NSCocoaErrorDomain.
I hope that's detailed enough to explain what I mean, not sure why the object being passed to log doesn't have the values I set up in the extension to DecodingError. I know I could easily just send across the additional user info separately in my call to Crashlytics, but I'd quite like to know where I'm going wrong with my understanding of this scenario.
NSError bridging is an interesting beast in the Swift compiler. On the one hand, NSError comes from the Foundation framework, which your application may or may not use; on the other, the actual bridging mechanics need to be performed in the compiler, and rightfully, the compiler should have as little knowledge of "high-level" libraries above the standard library as possible.
As such, the compiler has very little knowledge of what NSError actually is, and instead, Error exposes three properties which provide the entirety of the underlying representation of NSError:
public protocol Error {
var _domain: String { get }
var _code: Int { get }
// Note: _userInfo is always an NSDictionary, but we cannot use that type here
// because the standard library cannot depend on Foundation. However, the
// underscore implies that we control all implementations of this requirement.
var _userInfo: AnyObject? { get }
// ...
}
NSError, then, has a Swift extension which conforms to Error and implements those three properties:
extension NSError : Error {
#nonobjc
public var _domain: String { return domain }
#nonobjc
public var _code: Int { return code }
#nonobjc
public var _userInfo: AnyObject? { return userInfo as NSDictionary }
// ...
}
With this, when you import Foundation, any Error can be cast to an NSError and vice versa, as both expose _domain, _code, and _userInfo (which is what the compiler actually uses to perform the bridging).
The CustomNSError protocol plays into this by allowing you to supply an errorDomain, errorCode, and errorUserInfo, which are then exposed by various extensions as their underscore versions:
public extension Error where Self : CustomNSError {
/// Default implementation for customized NSErrors.
var _domain: String { return Self.errorDomain }
/// Default implementation for customized NSErrors.
var _code: Int { return self.errorCode }
// ...
}
So, how are EncodingError and DecodingError different? Well, since they're both defined in the standard library (which is present regardless of whether or not you use Foundation, and cannot depend on Foundation), they hook into the system by providing implementations of _domain, _code, and _userInfo directly.
Since both types provide the direct underscore versions of those variables, they don't call in to the non-underscore versions to get the domain, code, and user info — the values are used directly (rather than rely on var _domain: String { return Self.errorDomain }).
So, in effect, you can't override the behavior because EncodingError and DecodingError already provide this info. Instead, if you want to provide different codes/domains/user info dictionaries, you're going to need to write a function which takes an EncodingError/DecodingError and returns your own NSError, or similar.
Let me prefix with why I think this is not a duplicate of How to provide a localized description with an Error type in Swift?
The answers provided will still result in some static/class function call and not an initializer style or require casting to NSError (which I like to avoid).
A brief summery of the reasons:
A Swift function that throws does not declare the error type. We cannot enforce the catch to pass a custom type that simply conforms to the Error protocol. Knowing that, on the do-catch side, we get no help from the compiler as to what type (if custom) of error we get and by default we'll expect the known NSError properties. Otherwise, we need to simply rely on textual documentations explaining the type of errors we can catch, as the catch will simply pass an Error type.
Now, unlike NSError, Error is a protocol where the properties we get in userInfo are read only. So we resort to constructing an NSError type, and casting it as Error.
I am trying to create a simple clean struct that return an Error type (not NSError) where I can throw like:
throw MYError(domain: "com.somthing.error", code: 0, userInfo: [NSLocalizedDescriptionKey : "Something bad happened"])
Main issue is that the only way to set the NSLocalizedDescriptionKey, is by initializing an NSError object. Doing so will require casting to Error type (which is something I'm trying to avoid).
I first tried to use extension Error {..., but cannot use an initializer.
If I use a struct conforming to Error protocol (struct MyError: Error {...), I still have the problem of localizedDescription is get only.
What I use is actually something like:
struct MYError: Error {
static func with(domain: String = "com.somthing.error", code: Int = 0, localizedDescription: String) -> Error {
return NSError(domain: domain, code: code, userInfo: [NSLocalizedDescriptionKey : localizedDescription]) as Error
}
}
Which I can use like:
throw MYError.with(localizedDescription: "Some Error has happened")
Intuitively, I would expect a type like MYError to just use an initializer MYError(domain:..., not a static function.
The more Swifty way would be something like:
enum ErrorType: Error {
case one
case two
case three(with: String)
}
...
// In some function:
throw ErrorThrown.three(with: "Three!")
...
// Catch like:
catch ErrorType.three(let str) {
print("Error three: \(str)")
}
It is not clear if we're there yet. It seems that NSError is still much at play where I know I can expect to get a localizedDescription an optional localizedFailureReason and the familiar NSError properties.
Similarly as in How to provide a localized description with an Error type in Swift?
you can define a custom error type adopting the LocalizedError
protocol:
public struct MyError: Error {
let msg: String
}
extension MyError: LocalizedError {
public var errorDescription: String? {
return NSLocalizedString(msg, comment: "")
}
}
Example:
do {
throw MyError(msg: "Something happened")
} catch let error {
print(error.localizedDescription)
}
This prints the localized version of the given message.
Note that error in the catch-clause is a general Error, so
the caller does not need to cast it to the concrete error type (or even know
which error type is thrown).
Error is a protocol, you can throw anything which conforms to that protocol
For example
struct MYError : Error {
let description : String
let domain : String
var localizedDescription: String {
return NSLocalizedString(description, comment: "")
}
}
And you can use it:
func test() throws
{
throw MYError(description: "Some Error has happened", domain: "com.somthing.error")
}
do {
try test()
} catch let error as MYError{
print("error: ", error.domain, error.localizedDescription)
}
Finally found a way to stick anything in a generic Error type.
Thanks to vadian and Martin R, on which answers I finally came to this part.
Comes down to this:
struct MyError: Error {
var locString: String
var reason: String?
var code: Int
var wish: String
}
// The errorDescription is the only part that actually comes generically with Error
extension MyError: LocalizedError {
// This is actually part of LocalizedError
var errorDescription: String? {
return locString
}
}
extension Error {
var errorCode: Int {
return (self as! MyError).code
}
var failureReason: String? {
return (self as! MyError).reason
}
var everythingYouWishFor: String {
return (self as! MyError).wish
}
}
...
throw MyError(locString: "Localized string", reason: "The reason", code: 12, wish: "Best wishes")
I've got a rather large body of Swift 3 code for Mac OS 10.11 and up (using Xcode 8.2.1). There are a number of processes, among them a GUI application and a background service. Both of these use a custom URLProtocol, which is implemented in a framework (imported by both application and service). The protocol sometimes may generate instances of an enum that conforms to Error, which it catches and handles appropriately (generally by using the URLProtocolClient to toss them up to the URLSession trying to make the request).
When there's no error, both the app and the service work fine.
When there is an error, the app works fine (well, as expected).
When there is an error, the service crashes.
Wandering through the debugger has shown that this crash is occurring when the Error is automatically converted into an NSError by the runtime. I added this cast explicitly in my code, and sure enough, I get the same crash, now on that line.
I saw Swift 3.1: Crash when custom error is converted to NSError to access its domain property, but it doesn't apply here:
The solution there - extending the Error to a CustomNSError (and LocalizedError for good measure) and implementing the relevant properties - didn't help.
The crash occurs after the domain has been obtained; as far as I can tell that was not a problem for me.
Possibly relevant: that was on iOS, not Mac.
Having already tried the only listed solution to that question, I'm at something of a loss. I've been debugging this for hours without getting anywhere except that it seems to happen somewhere deep in the guts of dyld.
Code:
enum CrowbarProtocolError: Error {
case FailedToCreateSocket;
case PathTooLong;
case NonSocketFile;
case SocketNotFound;
...
case UnrecognizedError(errno: Int32);
}
extension CrowbarProtocolError: LocalizedError {
public var errorDescription: String? {
return "Some localized description"
}
}
extension CrowbarProtocolError: CustomNSError {
public static var errorDomain: String {
return "Some Domain Name"
}
public var errorCode: Int {
return 204 //Should be your custom error code.
}
public var errorUserInfo: [String: Any] {
return ["WTF": "Swift?"];
}
}
...
open class CrowbarUrlProtocol: URLProtocol {
...
override open func startLoading() {
do {
let sockHandle = try CrowbarUrlProtocol.openSocket();
let req = try buildRequestData();
sockHandle.write(req);
NotificationCenter.default.addObserver(
self,
selector: #selector(self.readCompleted),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil);
sockHandle.readToEndOfFileInBackgroundAndNotify();
} catch let err {
Log.warn("CrowbarUrlProtocol request failed with error \(err)");
// -- In the background service, THIS LINE CRASHES! --
let err2 = err as NSError;
Log.warn("As NSError: \(err2)");
// -- Without the explicit cast, it crashes on this line --
self.client?.urlProtocol(self, didFailWithError: err);
}
}
...
}
One idea I have for solving this is just doing everything (or, as much as possible) using NSErrors, on the grounds that if there's never a need to convert an Error to an NSError, then whatever is causing this crash won't happen. No idea if it'll work but it seems worth a try...
OK, as far as I can tell this is just a bug in Swift's runtime, but I found a work-around: just use NSError for everything involving Obj-C code rather than Swift Errors (to avoid the implicit cast). Since I already was implementing CustomNSError, it was easy to just create a toNSError() function on my Error enum, and use that for the self.client?.urlProtocol(self, didFailWithError: err) lines.
enum CrowbarProtocolError: Error, CustomNSError {
case FailedToCreateSocket;
case PathTooLong;
...
public func asNSError() -> NSError {
return NSError(domain: CrowbarProtocolError.errorDomain,
code: self.errorCode,
userInfo: self.errorUserInfo);
}
}
...
open class CrowbarUrlProtocol: URLProtocol {
...
override open func startLoading() {
do {
let sockHandle = try CrowbarUrlProtocol.openSocket();
let req = try buildRequestData();
sockHandle.write(req);
NotificationCenter.default.addObserver(
self,
selector: #selector(self.readCompleted),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil);
sockHandle.readToEndOfFileInBackgroundAndNotify();
} catch let err as CrowbarProtocolError {
Log.warn("CrowbarUrlProtocol request failed with error \(err)");
self.client?.urlProtocol(self, didFailWithError: err.asNSError());
}
catch let err {
Log.warn("CrowbarUrlProtocol caught non-CrowbarProtocol Error \(err)");
// This would probably crash the service, but shouldn't ever happen
self.client?.urlProtocol(self, didFailWithError: err);
}
}
...
}
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 7 years ago.
Improve this question
In Swift 2 any type that conforms to ErrorType protocol can be thrown and caught. For me, it makes sense to have a common error hierarchy and re-use it in multiple places. But, Apple documentation seems to push developers into using enums for handling errors.
For example, this hierarchy will let me catch and process a common ValidationError without knowing it's exact sub-type. This will also allow different parts of the application to extend ValidationError.
MyAppError
ValidationError
InvalidPathError
WrongFileTypeError
Mixing different styles of defining errors does not look like a good idea. So, should I model all error handling around class hierarchies or enums?
tl;dr
Enums are shorter, faster to write, easier to understand the potential errors, and the compiler will make sure you catch all of the errors.
The full story
ErrorType itself is just a empty protocol (there are hidden properties _code : Int and _domain : String but Apple takes care of that).
I quote from The Swift Programming Guide (link)
Swift enumerations are particularly well suited to modeling a group of
related error conditions, with associated values allowing for
additional information about the nature of an error to be
communicated.
To elaborate on that, enums allow you to express what exactly can go wrong. When doing error handling, you generally will have specific conditions that can fail (Swift pushes you in this direction with optionals and type safety).
Because errors are distinct cases you shouldn't really need many layers of inheritance (If you do add details to your answer please). Errors can be represented easily with enums. Using a large inheritance hierarchy is overly complicated.
Say you want every error to have a message that can be displayed to the user. Instead of a subclass you could just use a protocol.
protocol MyAppError : ErrorType {
var message: String { get }
}
Taking your given example a little further you would represent your ValidationError as an enum (as there are many validation errors).
enum ValidationError : MyAppError {
case InvalidPathError (String)
case WrongFileTypeError (expectedFileType: String)
var message: String {
switch self {
case .InvalidPathError(let invalidPath):
return "\(invalidPath) is an invalid path"
case .WrongFileTypeError(let expectedFileType):
return "Expected type of \(expectedFileType)"
}
}
}
_
func myFileFunction(path: String) throws {
guard let url = NSURL(string: path) else {
throw ValidationError.InvalidPathError(path)
}
guard let data = NSDictionary(contentsOfURL: url) else {
throw ValidationError.WrongFileTypeError(expectedFileType: ".plist")
}
print(data)
}
do {
try myFileFunction("hi.jpg")
} catch ValidationError.InvalidPathError(let path) {
print("Darn, had a bad path \(path)")
} catch ValidationError.WrongFileTypeError(let expectedType) {
print("Darn, expected the type \(expectedType)")
} catch (let error as MyAppError) {
print("Darn, some old error \(error.message)")
}
The compiler actually knows that the function will only throw ValidationErrors so it warns you if you try and catch MyAppError. Here a another/better way to do it.
do {
try myFileFunction("hi.jpg")
} catch (let error as ValidationError) {
switch error {
case .WrongFileTypeError(let expectedType):
print("Darn, expected the type \(expectedType)")
case .InvalidPathError(let path):
print("Darn, had a bad path \(path)")
}
}
Lets compare to the OO class/inheritance
class MyAppError : CustomStringConvertible {
let message: String
init(message: String) {
self.message = message
}
var description: String {
return message
}
}
class ValidationError : MyAppError {
}
class InvalidPathError : ValidationError {
let path: String
init(message: String, path: String) {
self.path = path
super.init(message: message)
}
override var description: String {
return "\(path) is an invalid path"
}
}
class WrongFileTypeError : ValidationError {
let expectedFileType: String
init(message: String, expectedFileType: String) {
self.expectedFileType = expectedFileType
super.init(message: message)
}
override var description: String {
return "Expected type of \(expectedFileType)"
}
}
_
func myFileFunction(path: String) throws {
guard let url = NSURL(string: path) else {
throw InvalidPathError(path: path)
}
guard let data = NSDictionary(contentsOfURL: url) else {
throw WrongFileTypeError(expectedFileType: ".plist")
}
print(data)
}
do {
try myFileFunction("hi.jpg")
} catch (let error as InvalidPathError) {
print("Darn, had a bad path \(error.path)")
} catch (let error as WrongFileTypeError) {
print("Darn, expected the type \(error.expectedFileType)")
} catch (let error as MyAppError) {
print("Some other error")
}
As you can see, it works but creating the error classes adds a lot of baggage. We also don't get the automatic namespacing from ValidationError.WrongFileTypeError or let error as ValidationError. All someone reading this class knows is that InvalidPathError and WrongFileTypeError are specifically caught. Contrast that to the enum version where you know ValidationError instances are being caught and THE COMPILER tells you this.