Why can any Error be unconditionally converted to NSError? - swift

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")
}

Related

How, in Swift, do I access a property with dynamic type checking, like Obj-C's id?

I have a function:
func logLocalisedDescription(_ error: Any) {
NSLog(error.localizedDescription)
}
I want this to work for NSError and SKError, both of which have a localizedDescription, but no common superclass or protocol that declares it.
Is there a way to tell the Swift compiler to do run-time rather than compile-time type checking, like Objective-C's id? I tried Any, AnyObject and AnyClass, but none of them worked.
I'm fine with the code failing at runtime if the property doesn't exist.
Please don't suggest retrofitting classes to adopt protocols or other solutions. I'm aware of those. The question is about how to do dynamic typing in Swift.
Use a protocol :)
protocol LocalizedDescribable {
var localizedDescription: String { get }
}
extension NSError: LocalizedDescribable {}
extension SKError: LocalizedDescribable {}
func logLocalizedDescription(_ error: LocalizedDescribable) {
NSLog(error.localizedDescription)
}
You're asking about sending arbitrary Objective-C messages to an object of unknown type. I would call that dynamic messaging rather than dynamic type checking. In Objective-C, there are ways to make any object handle any message, regardless of the object's type.
Swift (on Apple platforms) allows you to do Objective-C-style dynamic messaging to AnyObject, as documented under “Accessing Objective-C Methods and Properties” in the AnyObject reference. In this case, here's the syntax:
func logLocalizedDescription(_ error: Any) {
if let d = (error as AnyObject).localizedDescription as String? {
NSLog("%#", d)
}
}
I had to explicitly specify the type of the message here to avoid an “Ambigious use of 'localizedDescription'” error, because there are multiple definitions of localizedDescription with different types (most are String, but Progress.localizedDescription is String!).
However, you don't need to use dynamic messaging in this particular case. The preferred Swift way, in this case, is to use as? Error, like this:
func logLocalizedDescription(_ error: Any) {
if let error = error as? Error {
NSLog("%#", error.localizedDescription)
} else {
NSLog("unknown error: %#", String(describing: error))
}
}
As it happens, SKError already conforms to Error, although it's not documented to do so. The above function will print an SKError's localizedDescription.
If you don't want to rely on this undocumented conformance, you can explicitly make it conform:
extension SKError: Error { }
Since Error declares localizedDescription, and SKError already implements localizedDescription, this retroactive conformance requires no implementation of its own.
Note also that it's inappropriate to pass an unknown string (like an error's localizedDescription) as the first argument of NSLog, because the first argument of NSLog is a format string. If the string contains unexpected % characters, the runtime behavior is undefined.

Adopting CustomNSError in DecodingError

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.

Custom Error type initializer

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

Change Error localizedDescription [duplicate]

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

Swift protocol extensions for Value(Structures) types

public struct KZErrorInfo: Unboxable {
var statusCode = -1
var status: String?
var errorMessage: String?
public init() {
}
public init(unboxer: Unboxer) {
self.statusCode = unboxer.unbox("StatusCode")
self.status = unboxer.unbox("Status")
self.errorMessage = unboxer.unbox("Message")
}
}
protocol KZClientResponse: ETClientResponse {
var errorInfo: KZErrorInfo? { get set }
}
var errorInfo: KZErrorInfo? {
get {
if let value = objc_getAssociatedObject(self, &xoAssociationKeyErrorInfo) as? KZErrorInfo {
return value
}
return nil
}
set(newValue) {
if let error = newValue {
objc_setAssociatedObject(self, &xoAssociationKeyErrorInfo, error, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
My objective is to have a default implantation for the protocol KZClientResponse and Xcode is giving me a compile error as below. In the case of value types, how to overcome this issue? Appreciate you suggestions.
As the error message is indicating, objc_getAssociatedObject(_:_:) and objc_setAssociatedObject(_:_:_:_:) require AnyClass as the first argument. You cannot use Swift structs as AnyClass.
Think another way to store errorInfo which works with structs.
Why don't you have it as the struct's property?
... giving me a compile error as below. In the case of value types, how to overcome this issue?
You can't overcome the compiler error. You're trying to mix apples with oranges. objc_getAssociatedObject is, by definition, Objective-C. But Objective-C knows nothing of Swift structs; it cannot possibly see them. The only thing it knows about are what it calls objects — that is, classes and their instances. To work with a Swift struct, you cannot use the Objective-C runtime at all: you must operate entirely within Swift itself.