Confusing Error localizedDescription output

Swifters:
  I have a custom error enum:

enum MyError: Error {
    
    case network(error: Error)
    case json(error: Error)
    case value(error: Error)
    
    var localizedDescription: String {
        switch self {
        case let .network(error): return error.localizedDescription
        case let .json(error): return error.localizedDescription
        case let .value(error): return error.localizedDescription
        }
    }
    
}

However, the localizedDescription output is odd to me. When the value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence in all cases here? (This is Xcode 8.3b3).

Jon

1 Like

'localizedDescription' isn't a requirement of the Error protocol, which means it's not a customization point. You need to actually conform to LocalizedError and provide a valid 'errorDescription' instead.

(It's too bad that your second test works at all, since MyError doesn't conform to LocalizedError. Unfortunately NSError does conform to LocalizedError, and all Errors can be bridged to NSError.)

If you're interested, you can find more information about the design in its proposal, SE-0112 <https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md&gt;\.

Hope this helps,
Jordan

···

On Feb 28, 2017, at 12:54, Jon Shier via swift-users <swift-users@swift.org> wrote:

Swifters:
  I have a custom error enum:

enum MyError: Error {

   case network(error: Error)
   case json(error: Error)
   case value(error: Error)

   var localizedDescription: String {
       switch self {
       case let .network(error): return error.localizedDescription
       case let .json(error): return error.localizedDescription
       case let .value(error): return error.localizedDescription
       }
   }

}

However, the localizedDescription output is odd to me. When the value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence in all cases here? (This is Xcode 8.3b3).

Jon
_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users

Sorry, I forgot that part; MyError does conform to LocalizedError. However, even if it didn’t, Error has a localizedDescription property, so my question was why my implementation of that property wasn’t used in favor of the generated one. Or is that not the case for properties that aren’t required by the protocol? Even with the conformance, treating MyError as Error or LocalizedError doesn’t return the correct value for the property. localizedDescription seems to work fine when talking to Error instances backed by NSError though. This behavior doesn’t seem to be correct, otherwise there’s no way to get a useful description out of a custom Error without also implementing LocalizedError and treating the value as that type instead.

Jon

···

On Feb 28, 2017, at 4:10 PM, Jordan Rose <jordan_rose@apple.com> wrote:

'localizedDescription' isn't a requirement of the Error protocol, which means it's not a customization point. You need to actually conform to LocalizedError and provide a valid 'errorDescription' instead.

(It's too bad that your second test works at all, since MyError doesn't conform to LocalizedError. Unfortunately NSError does conform to LocalizedError, and all Errors can be bridged to NSError.)

If you're interested, you can find more information about the design in its proposal, SE-0112 <https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md&gt;\.

Hope this helps,
Jordan

On Feb 28, 2017, at 12:54, Jon Shier via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Swifters:
  I have a custom error enum:

enum MyError: Error {

   case network(error: Error)
   case json(error: Error)
   case value(error: Error)

   var localizedDescription: String {
       switch self {
       case let .network(error): return error.localizedDescription
       case let .json(error): return error.localizedDescription
       case let .value(error): return error.localizedDescription
       }
   }

}

However, the localizedDescription output is odd to me. When the value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence in all cases here? (This is Xcode 8.3b3).

Jon
_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

Something strange I just observed. This version of MyError properly prints its localized description (in a Playground).

enum MyError: Error {
    
    case network
    case json
    case value
    
    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }
    
    var errorDescription: String? { return localizedDescription }
    
}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): third

However, this version does not:

enum MyError: Error {
    
    case network
    case json
    case value
    
    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }
    
    var errorDescription: String? { return localizedDescription }
    
}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): The operation couldn’t be completed. (__lldb_expr_103.MyError error 2.)

Surely that’s a bug?

Jon

···

On Feb 28, 2017, at 4:32 PM, Jon Shier <jon@jonshier.com> wrote:

Sorry, I forgot that part; MyError does conform to LocalizedError. However, even if it didn’t, Error has a localizedDescription property, so my question was why my implementation of that property wasn’t used in favor of the generated one. Or is that not the case for properties that aren’t required by the protocol? Even with the conformance, treating MyError as Error or LocalizedError doesn’t return the correct value for the property. localizedDescription seems to work fine when talking to Error instances backed by NSError though. This behavior doesn’t seem to be correct, otherwise there’s no way to get a useful description out of a custom Error without also implementing LocalizedError and treating the value as that type instead.

Jon

On Feb 28, 2017, at 4:10 PM, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

'localizedDescription' isn't a requirement of the Error protocol, which means it's not a customization point. You need to actually conform to LocalizedError and provide a valid 'errorDescription' instead.

(It's too bad that your second test works at all, since MyError doesn't conform to LocalizedError. Unfortunately NSError does conform to LocalizedError, and all Errors can be bridged to NSError.)

If you're interested, you can find more information about the design in its proposal, SE-0112 <https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md&gt;\.

Hope this helps,
Jordan

On Feb 28, 2017, at 12:54, Jon Shier via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Swifters:
  I have a custom error enum:

enum MyError: Error {

   case network(error: Error)
   case json(error: Error)
   case value(error: Error)

   var localizedDescription: String {
       switch self {
       case let .network(error): return error.localizedDescription
       case let .json(error): return error.localizedDescription
       case let .value(error): return error.localizedDescription
       }
   }

}

However, the localizedDescription output is odd to me. When the value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence in all cases here? (This is Xcode 8.3b3).

Jon
_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

Guh, second version should be this (Discourse save me).

enum MyError: Error {
    
    case network
    case json
    case value
    
    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }
    
}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription)

···

On Feb 28, 2017, at 4:36 PM, Jon Shier <jon@jonshier.com> wrote:

Something strange I just observed. This version of MyError properly prints its localized description (in a Playground).

enum MyError: Error {
    
    case network
    case json
    case value
    
    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }
    
    var errorDescription: String? { return localizedDescription }
    
}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): third

However, this version does not:

enum MyError: Error {
    
    case network
    case json
    case value
    
    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }
    
    var errorDescription: String? { return localizedDescription }
    
}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): The operation couldn’t be completed. (__lldb_expr_103.MyError error 2.)

Surely that’s a bug?

Jon

On Feb 28, 2017, at 4:32 PM, Jon Shier <jon@jonshier.com <mailto:jon@jonshier.com>> wrote:

Sorry, I forgot that part; MyError does conform to LocalizedError. However, even if it didn’t, Error has a localizedDescription property, so my question was why my implementation of that property wasn’t used in favor of the generated one. Or is that not the case for properties that aren’t required by the protocol? Even with the conformance, treating MyError as Error or LocalizedError doesn’t return the correct value for the property. localizedDescription seems to work fine when talking to Error instances backed by NSError though. This behavior doesn’t seem to be correct, otherwise there’s no way to get a useful description out of a custom Error without also implementing LocalizedError and treating the value as that type instead.

Jon

On Feb 28, 2017, at 4:10 PM, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

'localizedDescription' isn't a requirement of the Error protocol, which means it's not a customization point. You need to actually conform to LocalizedError and provide a valid 'errorDescription' instead.

(It's too bad that your second test works at all, since MyError doesn't conform to LocalizedError. Unfortunately NSError does conform to LocalizedError, and all Errors can be bridged to NSError.)

If you're interested, you can find more information about the design in its proposal, SE-0112 <https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md&gt;\.

Hope this helps,
Jordan

On Feb 28, 2017, at 12:54, Jon Shier via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Swifters:
  I have a custom error enum:

enum MyError: Error {

   case network(error: Error)
   case json(error: Error)
   case value(error: Error)

   var localizedDescription: String {
       switch self {
       case let .network(error): return error.localizedDescription
       case let .json(error): return error.localizedDescription
       case let .value(error): return error.localizedDescription
       }
   }

}

However, the localizedDescription output is odd to me. When the value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence in all cases here? (This is Xcode 8.3b3).

Jon
_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

It's how protocols work in Swift.

errorDescription is a protocol requirement of LocalizedError (that is, it's part of the protocol definition, not just added in an extension). Only protocol requirements are dynamically dispatched.

Take a look at the implementation of Error.localizedDescription in the stdlib [1]:

public extension Error {
   /// Retrieve the localized description for this error.
   var localizedDescription: String {
     return (self as NSError).localizedDescription
   }
}

So Error.localizedDescription casts the value to NSError. And NSError's localizedDescription implementation presumably calls errorDescription (via dynamic dispatch), which ends up calling your code.

In your second example, Error.localizedDescription is _not_ a protocol requirement and therefore only statically dispatched. One (_the_?) reason for this discrepancy is that only methods defined in the original protocol definition get an entry in the protocol's witness table — since anybody can add extensions later, there's no good way for the compiler to reserve space for methods defined in extensions.

This time, NSError.localizedDescription doesn't dispatch to your implementation because it can't be reached via dynamic dispatch.

It's a confusing interaction between Swift's protocol dispatch rules and the automatic casting of Error to NSError, which then ends up calling LocalizedError.errorDescription.

[1]: https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/NSError.swift#L203-L208

···

On 28/02/2017 22:37, Jon Shier via swift-users wrote:

Guh, second version should be this (Discourse save me).

enum MyError: Error {

    case network
    case json
    case value

    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }

}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription)

On Feb 28, 2017, at 4:36 PM, Jon Shier <jon@jonshier.com >> <mailto:jon@jonshier.com>> wrote:

Something strange I just observed. This version of MyError properly
prints its localized description (in a Playground).

enum MyError: Error {

    case network
    case json
    case value

    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }

    var errorDescription: String? { return localizedDescription }

}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): third

However, this version does not:

enum MyError: Error {

    case network
    case json
    case value

    var localizedDescription: String {
        switch self {
        case .network: return "first"
        case .json: return "second"
        case .value: return "third"
        }
    }

    var errorDescription: String? { return localizedDescription }

}

extension MyError: LocalizedError { }

print((MyError.value as Error).localizedDescription): The operation
couldn’t be completed. (__lldb_expr_103.MyError error 2.)

Surely that’s a bug?

Jon

On Feb 28, 2017, at 4:32 PM, Jon Shier <jon@jonshier.com >>> <mailto:jon@jonshier.com>> wrote:

Sorry, I forgot that part; MyError does conform to LocalizedError.
However, even if it didn’t, Error has a localizedDescription
property, so my question was why my implementation of that property
wasn’t used in favor of the generated one. Or is that not the case
for properties that aren’t required by the protocol? Even with the
conformance, treating MyError as Error or LocalizedError doesn’t
return the correct value for the property. localizedDescription seems
to work fine when talking to Error instances backed by NSError
though. This behavior doesn’t seem to be correct, otherwise there’s
no way to get a useful description out of a custom Error without also
implementing LocalizedError and treating the value as that type instead.

Jon

On Feb 28, 2017, at 4:10 PM, Jordan Rose <jordan_rose@apple.com >>>> <mailto:jordan_rose@apple.com>> wrote:

'localizedDescription' isn't a requirement of the Error protocol,
which means it's not a customization point. You need to actually
conform to LocalizedError and provide a valid 'errorDescription'
instead.

(It's too bad that your second test works at all, since MyError
doesn't conform to LocalizedError. Unfortunately NSError
/does/ conform to LocalizedError, and all Errors can be bridged to
NSError.)

If you're interested, you can find more information about the design
in its proposal, SE-0112
<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md&gt;\.

Hope this helps,
Jordan

On Feb 28, 2017, at 12:54, Jon Shier via swift-users >>>>> <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Swifters:
I have a custom error enum:

enum MyError: Error {

   case network(error: Error)
   case json(error: Error)
   case value(error: Error)

   var localizedDescription: String {
       switch self {
       case let .network(error): return error.localizedDescription
       case let .json(error): return error.localizedDescription
       case let .value(error): return error.localizedDescription
       }
   }

}

However, the localizedDescription output is odd to me. When the
value is treated as:

Error: po error.localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

LocalizedError: po (error as! LocalizedError).localizedDescription
"The operation couldn’t be completed. (MyApp.MyError error 1.)”

MyError: Error: po (error as! MyError).localizedDescription
"JSON could not be serialized because of error:\nThe data couldn’t
be read because it isn’t in the correct format."

Shouldn’t my implementation of localizedDescription take precedence
in all cases here? (This is Xcode 8.3b3).

Jon
_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

2 Likes