[Discussion] Adopting a new common error type outside the bounds of NSError


(Erica Sadun) #1

NSError encapsulates runtime error condition information. It uses an historic, time-tested object
built to communicate information to users and provide a basis for workarounds.
Swift's redesigned error mechanism differs significantly from NSError in that its primary consumer
are API calls, via the try-catch mechanism and not end-users.

I would not like Swift to be tied down to an archaic construct for the sake of consistency. NSError's
core domain/code/userInfo attributes are architected to archaic use-cases. It domains of
Mach/POSIX/Carbon/Cocoa are subsumed by Swift modules. The integer-based codes
can be better represented by plain-text strings, the dictionary keys model usage that
poorly integrates into Swift's throw-try ecosystem.

To me, an error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of the error?

A universal standard library error could be as simple as

struct Error: ErrorType {
  let reason: String
}

although, I'd far prefer to add a context, using the newly updated debug literals to describe
exactly where the error sourced from. An ideal context <http://ericasadun.com/2015/08/27/capturing-context-swiftlang/> would include:

* A source location including a fully qualified module, file name and line number, shared
  object (dsohandle), symbol, e.g. FooType.methodName(label1: _, label2: _) (with an optional
  mangled name component), and column number (although I sincerely do not understand
   the point of column number)
* An indicator of release or debug build
* Pre-crafted strings that combine these data into printable forms suitable for release and
  debug output using brief, long, and exhaustive context forms.
* Decomposable elements, offering both the full context story while retaining an option
  to access and query individual components as needed.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
}

further, I'd want to implement some kind of generalizable dictionary, an infoDictionary
rather than a userDictionary, that describes error circumstances and possible recovery
selectors, without being tied to the notion that the ultimate consumer <https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html#//apple_ref/doc/uid/TP40001806-CH202-CJBGAIBJ> is an NSAlert
presented to a user.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
  let infoDictionary: Dictionary<String: Any>
}

-- Erica


(David Owens II) #2

NSError' domains and code create a universal mechanism to filter errors from logs by semantic grouping. Your new proposed type lacks this fairly important quality.

Is the intent that the reason string to be presented to the user? If so, that's not a great story for localization.

As for context, release vs debug is not a sufficient breakdown. A lot of software is built with much more granularity that just those two options. Such information included build numbers, branching, flags to light up experimental features, etc...

As for column number, you'll be happy to have it for the cases you have two throwing calls on the same line.

-David

···

Sent from my iPhone

On Mar 6, 2016, at 9:17 AM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

NSError encapsulates runtime error condition information. It uses an historic, time-tested object
built to communicate information to users and provide a basis for workarounds.
Swift's redesigned error mechanism differs significantly from NSError in that its primary consumer
are API calls, via the try-catch mechanism and not end-users.

I would not like Swift to be tied down to an archaic construct for the sake of consistency. NSError's
core domain/code/userInfo attributes are architected to archaic use-cases. It domains of
Mach/POSIX/Carbon/Cocoa are subsumed by Swift modules. The integer-based codes
can be better represented by plain-text strings, the dictionary keys model usage that
poorly integrates into Swift's throw-try ecosystem.

To me, an error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of the error?

A universal standard library error could be as simple as

struct Error: ErrorType {
  let reason: String
}

although, I'd far prefer to add a context, using the newly updated debug literals to describe
exactly where the error sourced from. An ideal context would include:

* A source location including a fully qualified module, file name and line number, shared
  object (dsohandle), symbol, e.g. FooType.methodName(label1: _, label2: _) (with an optional
  mangled name component), and column number (although I sincerely do not understand
   the point of column number)
* An indicator of release or debug build
* Pre-crafted strings that combine these data into printable forms suitable for release and
  debug output using brief, long, and exhaustive context forms.
* Decomposable elements, offering both the full context story while retaining an option
  to access and query individual components as needed.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
}

further, I'd want to implement some kind of generalizable dictionary, an infoDictionary
rather than a userDictionary, that describes error circumstances and possible recovery
selectors, without being tied to the notion that the ultimate consumer is an NSAlert
presented to a user.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
  let infoDictionary: Dictionary<String: Any>
}

-- Erica

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


(Charles Kissinger) #3

NSError encapsulates runtime error condition information. It uses an historic, time-tested object
built to communicate information to users and provide a basis for workarounds.
Swift's redesigned error mechanism differs significantly from NSError in that its primary consumer
are API calls, via the try-catch mechanism and not end-users.

I would not like Swift to be tied down to an archaic construct for the sake of consistency. NSError's
core domain/code/userInfo attributes are architected to archaic use-cases. It domains of
Mach/POSIX/Carbon/Cocoa are subsumed by Swift modules. The integer-based codes
can be better represented by plain-text strings, the dictionary keys model usage that
poorly integrates into Swift's throw-try ecosystem.

To me, an error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of the error?

A universal standard library error could be as simple as

struct Error: ErrorType {
  let reason: String
}

Erica,

I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)

Would your idea work better as a “StandardErrorProtocol” with a default implementation for “context” that provides the source location, etc.? (I have no idea how much compiler magic that might require.)

—CK

···

On Mar 6, 2016, at 9:17 AM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

although, I'd far prefer to add a context, using the newly updated debug literals to describe
exactly where the error sourced from. An ideal context <http://ericasadun.com/2015/08/27/capturing-context-swiftlang/> would include:

* A source location including a fully qualified module, file name and line number, shared
  object (dsohandle), symbol, e.g. FooType.methodName(label1: _, label2: _) (with an optional
  mangled name component), and column number (although I sincerely do not understand
   the point of column number)
* An indicator of release or debug build
* Pre-crafted strings that combine these data into printable forms suitable for release and
  debug output using brief, long, and exhaustive context forms.
* Decomposable elements, offering both the full context story while retaining an option
  to access and query individual components as needed.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
}

further, I'd want to implement some kind of generalizable dictionary, an infoDictionary
rather than a userDictionary, that describes error circumstances and possible recovery
selectors, without being tied to the notion that the ultimate consumer <https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html#//apple_ref/doc/uid/TP40001806-CH202-CJBGAIBJ> is an NSAlert
presented to a user.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
  let infoDictionary: Dictionary<String: Any>
}

-- Erica

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


(Lily Ballard) #4

I can see the benefit in having a basic error type for use with alerts
and such, but arguably that's what NSError is for (although NSError
doesn't pick up context info). A downside to the type you're proposing
is it doesn't provide any way to programmatically indicate what went
wrong (the strings are presumably for human consumption, not
programmatic introspection), which makes it largely useless for anything
except the point where you're about to hand off an error to the UI for
display (where it's too late to gather context info).

Also I disagree that the codes can be better represented as strings.
Strings are presumably for human consumption, but error codes are for
programmatic consumption. You don't want to do string comparisons to
figure out what type of error it is. And of course codes themselves are
obsoleted by having strong type information, which is what Swift already
has with concrete ErrorType implementations.

-Kevin Ballard

···

On Sun, Mar 6, 2016, at 09:17 AM, Erica Sadun via swift-evolution wrote:

NSError encapsulates runtime error condition information. It uses an
historic, time-tested object built to communicate information to users
and provide a basis for workarounds. Swift's redesigned error
mechanism differs significantly from NSError in that its primary
consumer are API calls, via the try-catch mechanism and not end-users.

I would not like Swift to be tied down to an archaic construct for the
sake of consistency. NSError's core domain/code/userInfo attributes
are architected to archaic use-cases. It domains of
Mach/POSIX/Carbon/Cocoa are subsumed by Swift modules. The integer-
based codes can be better represented by plain-text strings, the
dictionary keys model usage that poorly integrates into Swift's throw-
try ecosystem.

To me, an error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of
  the error?

A universal standard library error could be as simple as

struct Error: ErrorType { let reason: String }

although, I'd far prefer to add a context, using the newly updated
debug literals to describe exactly where the error sourced from. An
ideal context[1] would include:

* A source location including a fully qualified module, file name and
  line number, shared object (dsohandle), symbol, e.g.
  FooType.methodName(label1: _, label2: _) (with an optional mangled
  name component), and column number (although I sincerely do not
  understand the point of column number)
* An indicator of release or debug build
* Pre-crafted strings that combine these data into printable forms
  suitable for release and debug output using brief, long, and
  exhaustive context forms.
* Decomposable elements, offering both the full context story while
  retaining an option to access and query individual components as
  needed.

struct Error: ErrorType { let reason: String let context:
ContextType }

further, I'd want to implement some kind of generalizable
dictionary, an infoDictionary rather than a userDictionary, that
describes error circumstances and possible recovery selectors,
without being tied to the notion that the ultimate consumer[2] is an
NSAlert presented to a user.

struct Error: ErrorType { let reason: String let context:
ContextType let infoDictionary: Dictionary<String: Any> }

-- Erica

_________________________________________________
swift-evolution mailing list swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Links:

  1. http://ericasadun.com/2015/08/27/capturing-context-swiftlang/
  2. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html#//apple_ref/doc/uid/TP40001806-CH202-CJBGAIBJ


(Erica Sadun) #5

My primary consumer is the developer and does not exclude use of NSError or traditional Cocoa pathways.

-- E

···

On Mar 6, 2016, at 12:32 PM, David Owens II <david@owensd.io> wrote:

NSError' domains and code create a universal mechanism to filter errors from logs by semantic grouping. Your new proposed type lacks this fairly important quality.

Is the intent that the reason string to be presented to the user? If so, that's not a great story for localization.

As for context, release vs debug is not a sufficient breakdown. A lot of software is built with much more granularity that just those two options. Such information included build numbers, branching, flags to light up experimental features, etc...

As for column number, you'll be happy to have it for the cases you have two throwing calls on the same line.

-David

Sent from my iPhone

On Mar 6, 2016, at 9:17 AM, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

NSError encapsulates runtime error condition information. It uses an historic, time-tested object
built to communicate information to users and provide a basis for workarounds.
Swift's redesigned error mechanism differs significantly from NSError in that its primary consumer
are API calls, via the try-catch mechanism and not end-users.

I would not like Swift to be tied down to an archaic construct for the sake of consistency. NSError's
core domain/code/userInfo attributes are architected to archaic use-cases. It domains of
Mach/POSIX/Carbon/Cocoa are subsumed by Swift modules. The integer-based codes
can be better represented by plain-text strings, the dictionary keys model usage that
poorly integrates into Swift's throw-try ecosystem.

To me, an error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of the error?

A universal standard library error could be as simple as

struct Error: ErrorType {
  let reason: String
}

although, I'd far prefer to add a context, using the newly updated debug literals to describe
exactly where the error sourced from. An ideal context <http://ericasadun.com/2015/08/27/capturing-context-swiftlang/> would include:

* A source location including a fully qualified module, file name and line number, shared
  object (dsohandle), symbol, e.g. FooType.methodName(label1: _, label2: _) (with an optional
  mangled name component), and column number (although I sincerely do not understand
   the point of column number)
* An indicator of release or debug build
* Pre-crafted strings that combine these data into printable forms suitable for release and
  debug output using brief, long, and exhaustive context forms.
* Decomposable elements, offering both the full context story while retaining an option
  to access and query individual components as needed.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
}

further, I'd want to implement some kind of generalizable dictionary, an infoDictionary
rather than a userDictionary, that describes error circumstances and possible recovery
selectors, without being tied to the notion that the ultimate consumer <https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html#//apple_ref/doc/uid/TP40001806-CH202-CJBGAIBJ> is an NSAlert
presented to a user.

struct Error: ErrorType {
  let reason: String
  let context: ContextType
  let infoDictionary: Dictionary<String: Any>
}

-- Erica

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


(Brent Royal-Gordon) #6

I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)

I agree. A single concrete error type with stringly-typed data inside it is the precise *opposite* of how the error mechanism should be used.

Swift errors use types and cases to clearly categorize errors and make them easy to programmatically match; they then use associated values to explicitly model the details of each type of error, and pattern matching to allow `catch` clauses to easily zero in on specific errors they can handle, no matter how finicky the requirements. When you start throwing around concrete `Error` structs with `reason` strings and unstructured bags of data called `infoDictionary`, you severely handicap every one of those features.

In general, I think this is tackling the wrong problem. Each error type can manage its own storage containing whatever that type happens to need. Swift will automatically copy or move that information around as appropriate. Simply put, there's nothing we need to do there.

There are, however, two broad categories of things I *do* think should be modeled better:

- The mapping between a Swift Error and an NSError.
- The way errors are presented to the user.

So here's what I suggest we do to Error in Swift 3.

Clarify and Formalize NSError Bridging

···

----------------------------------------------------

Currently, the only requirements of `Error` are the hidden `_domain` and `_code` types. Let's formalize those. We'll also add support for `userInfo`, and include a constructor so we can unpack `userInfo` in the other direction.

  public protocol Error {
    var domain: String { get }
    @_numbered var code: Int { get }
    var userInfo: [String: AnyObject] { get }
    
    init(code: Int, userInfo: [String: AnyObject])
  }

  extension Error {
    public var domain: String {
      return String(reflecting: self)
    }
    
    public var userInfo: [String: AnyObject] {
      return [:]
    }
    
    @_numbered init(code: Int, userInfo: [String: AnyObject]) {
      self.init(_numbered: code)
    }
  }
  
  public extension Any<Error> {
    public init(_: NSError) { ...omitted... }
  }
  
  public extension NSError {
    public init(_: Error) { ...omitted... }
  }

`code` is still magically synthesized, as indicated by the imaginary `@_numbered` attribute. I don't like that very much, but I don't see a good alternative to it, either—RawRepresentable synthesis *almost* works, except that cases with associated values wouldn't be supported.

CustomStringConvertible for Simple Error Messages
----------------------------------------------------------------------

I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:

  enum HollywoodCarError: Error, CustomStringConvertible {
    case exploded
    case engineFellOut
    
    var description: String {
      switch self {
      case .exploded:
        return localized("The car has exploded in a fireball.")
      case .engineFellOut:
        return localized("The car's engine has fallen out.")
      }
    }
  }

(The `CustomStringConvertible` documentation is not entirely clear on whether `description` should be user-readable and localized. I interpret it to be "yes", but if not, we can provide a `LocalizedStringConvertible` protocol along the same lines. In either case, the point is that this is *not* a feature limited to errors; there really ought to be a protocol for anything which can be shown to users in a plain-text form.)

PresentableError
-----------------------

There are two other parts of NSError that I think we should widely model because they'd be useful on all platforms.

One is the ability to provide a more detailed error message. NSError calls this the `localizedRecoverySuggestion`; I recommend we just call it `suggestion` or perhaps `details`.

The other is the ability to retrieve a terse sentence fragment explaining the error's cause, which can be inserted into a larger paragraph. NSError calls this `localizedFailureReason`; I suggest simply using `reason`.

Both of these fields should be bridged into their NSError equivalents, and conforming to their protocol would also imply conformance to `CustomStringConvertible`.

So here is what I would suggest:

  protocol PresentableError: Error, CustomStringConvertible {
    var suggestion: String { get }
    var reason: String { get }
  }

Note that you can declare merely `PresentableError` in your error type, since it implies conformance to `Error`:

  enum HollywoodCarError: PresentableError {
    ...as before...
    
    var reason: String {
      switch self {
      case .exploded:
        return localized("It gently tapped something.")
      case .engineFellOut:
        return localized("The writers want you to get into some wacky hijinks.")
    }
    
    var suggestion: String {
      switch self {
      case .exploded:
        return localized("Put on sunglasses and walk away from the explosion at a measured pace.")
      case .engineFellOut:
        return localized("Hike to the nearest town and check into the only motel.")
      }
    }
  }
    
RecoverableError
-----------------------

Lower-priority because it's not used *that* often, but I think it's worth modeling in the long run. It may make sense to put this in Foundation instead of stdlib, though.

The idea behind Cocoa's `recoveryAttempter` is good, but I think the design is a problem, especially in Corelibs Foundation where we won't be able to perform selectors. So I suggest an alternative design. I believe it can be bridged to NSError's approach, although it would not quite be a *trivial* bridging process.

First, the RecoveryAttempting protocol, which models one particular approach to recovering from the error:

  // This is class-constrained in case `recover(from:completion:)` needs to be mutating.
  public protocol RecoveryAttempting: class, CustomStringConvertible {
    // The CustomStringConvertible name should be suitable for buttons.
    
    var destructive: Bool
    
    /// Attempt to recover from the error using this attempter. Calls `completion` when finished.
    func recover(from error: Error, completion: (Bool, ErrorType?) -> Void)
  }
  extension RecoveryAttempting {
    public var destructive: Bool { return false }
  }

Second, the RecoverableError protocol:

  public protocol RecoverableError: Error {
    var recoveryAttempters: [Any<RecoveryAttempting>] { get }
  }

We should document that you should only provide non-trivial recovery attempters (i.e. no Cancel options). Higher-level error presentation code can always do that.

Aside: What We Leave Behind
----------------------------------------

`helpAnchor` is not modeled because it is *extremely* platform-specific. (It might make sense for AppKit to provide a protocol to represent it, though.)

Programmatically useful data about the error, like `NSURLErrorKey` or even `NSUnderlyingErrorKey`, are not modeled because that information is only available from particular error types or even individual errors. Without knowing at least *something* about the error you're handling, it's hard to do anything useful with random pieces of information like this.

Concrete Convenience Types
----------------------------------------

Although so far I've only talked about providing protocols, I think there are some concrete types we should provide as well. For these I will provide only interfaces; the implementations should be reasonably straightforward.

The first is a concrete type conforming to `RecoveryAttempting` which takes a closure:

  public final class RecoveryAttempter: RecoveryAttempting {
    typealias Attempter = (ErrorType, (Bool, ErrorType?) -> Void) -> Void
    public init(description: String, destructive: Bool = false, attempter: Attempter)
  }

I would also like to offer wrapper types which allow you to easily change various aspects of error presentation, along these lines:

  struct CustomizedError<ErrorType: Error>: Error, CustomStringConvertible {
    init(_ original: ErrorType, description: String)
  }
  extension CustomizedError: PresentableError where ErrorType: PresentableError {}
  extension CustomizedError: PresentableError where ErrorType: PresentableError {}
  
  struct CustomizedPresentableError<ErrorType: Error>: PresentableError {
    init(_ original: ErrorType, description: String, reason: String, suggestion: String)
  }
  extension CustomizedPresentableError where ErrorType: PresentableError {
    init(_ original: ErrorType, description: String? = nil, reason: String? = nil, suggestion: String? = nil)
  }
  extension CustomizedPresentableError: RecoverableError where ErrorType: RecoverableError {}
  
  struct AdditionalRecoveryError<ErrorType: Error>: RecoverableError {
    init(_ original: ErrorType, recoveryAttempters: [RecoveryAttempter])
  }
  extension AdditionalRecoveryError where ErrorType: RecoverableError {
    init(_ original: ErrorType, additionalRecoveryAttempters: [RecoveryAttempter])
  }
  extension AdditionalRecoveryError: CustomStringConvertible where ErrorType: CustomStringConvertible {}
  extension AdditionalRecoveryError: PresentableError where ErrorType: PresentableError {}

However, these types present a significant challenge: They interfere with `catch` matching. One possible workaround would be to have `Error` itself include an `original` property and have `catch` match against that; most `Error` types would return `self`, but shallow, non-semantic wrappers like these would return the error they were wrapping. (Note that this is *not* the same as Foundation's `underlyingError`, which is why it's named differently.) This could make it a little tricky to rethrow errors, though—doing so might strip away the custom behavior wrapped around the error.

This could be interpreted as a sign that the problem needs to be factored differently by separating the *error* from the *error message*. But I don't see a clean way to do that; it would make implementing an `Error` type more cumbersome, it would still not solve the wrapping problem (unless the idea is that you should only tweak an error message once you're committed to presenting it), and it would create an impedance mismatch with `NSError`.

--
Brent Royal-Gordon
Architechies


(Adam Sharp) #7

A column number could also potentially be useful in an editor to jump directly to the location of a fatal error.

–Adam

···

On 7 Mar 2016, at 4:17 AM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

A source location including a fully qualified module, ..., and column number (although I sincerely do not understand the point of column number)

On 7 Mar 2016, at 6:32 AM, David Owens II via swift-evolution <swift-evolution@swift.org> wrote:

As for column number, you'll be happy to have it for the cases you have two throwing calls on the same line.


(Dave Abrahams) #8

Are you sure? People often reflexively think they want strong static
typing in their errors, forgetting that error handling of the
throw/catch variety is fundamentally dynamically typed, effectively
introspecting the error at the catch site. Think about the use cases.

···

on Sun Mar 06 2016, Charles Kissinger <swift-evolution@swift.org> wrote:

Erica,

I wouldn’t want a single, universal error type for the standard
library. I would want to be able to selectively catch errors based on
their type rather than having to look into the “reason” string to
determine what happened. (In other words, the reason should be encoded
in the error type.)

--
-Dave


(Erica Sadun) #9

Oh dear, because that is exactly *not* what I was proposing. I was suggesting something that was a standard fallback
error completely separate from any other modification to the error mechanism, just because it's extremely useful
to have something to throw without having to design a full error system for things like command-line utilities, etc.
In otherwords, a pre-constructed vanilla handy-error, ready for use that would have zero impact on any other error
system, implementation, or mechanism.

-- Erica

···

On Mar 6, 2016, at 7:38 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)

I agree. A single concrete error type with stringly-typed data inside it is the precise *opposite* of how the error mechanism should be used.


(Lily Ballard) #10

> I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)

I agree. A single concrete error type with stringly-typed data inside it is the precise *opposite* of how the error mechanism should be used.

Swift errors use types and cases to clearly categorize errors and make them easy to programmatically match; they then use associated values to explicitly model the details of each type of error, and pattern matching to allow `catch` clauses to easily zero in on specific errors they can handle, no matter how finicky the requirements. When you start throwing around concrete `Error` structs with `reason` strings and unstructured bags of data called `infoDictionary`, you severely handicap every one of those features.

In general, I think this is tackling the wrong problem. Each error type can manage its own storage containing whatever that type happens to need. Swift will automatically copy or move that information around as appropriate. Simply put, there's nothing we need to do there.

There are, however, two broad categories of things I *do* think should be modeled better:

- The mapping between a Swift Error and an NSError.
- The way errors are presented to the user.

So here's what I suggest we do to Error in Swift 3.

Clarify and Formalize NSError Bridging
----------------------------------------------------

Currently, the only requirements of `Error` are the hidden `_domain` and `_code` types. Let's formalize those. We'll also add support for `userInfo`, and include a constructor so we can unpack `userInfo` in the other direction.

  public protocol Error {
    var domain: String { get }
    @_numbered var code: Int { get }
    var userInfo: [String: AnyObject] { get }
    
    init(code: Int, userInfo: [String: AnyObject])
  }

  extension Error {
    public var domain: String {
      return String(reflecting: self)
    }
    
    public var userInfo: [String: AnyObject] {
      return [:]
    }
    
    @_numbered init(code: Int, userInfo: [String: AnyObject]) {
      self.init(_numbered: code)
    }
  }
  
  public extension Any<Error> {
    public init(_: NSError) { ...omitted... }
  }
  
  public extension NSError {
    public init(_: Error) { ...omitted... }
  }

`code` is still magically synthesized, as indicated by the imaginary `@_numbered` attribute. I don't like that very much, but I don't see a good alternative to it, either—RawRepresentable synthesis *almost* works, except that cases with associated values wouldn't be supported.

I disagree that this should be part of Error. `domain` and `code` are both relics of NSError integration and have no purpose when not bridging to NSError. The `domain` is really just a string representation of the error type, and the `code` is just an integral representation of the enum variant, so they're just imperfect representations of information already contained by the error value itself. We do actually need to include them in order to bridge any arbitrary Error to NSError, but they should stay as hidden implementation details (e.g. `_domain` and `_code`) like they are today. Besides, the names `domain` and `code` might be names that the concrete Error implementation actually wants to use for its own purposes.

Also, there's no need to ever "unpack" userInfo. When you bridge a Swift error into NSError, it uses a private NSError subclass _SwiftNativeNSError, which boxes up the original error, and when that NSError is bridged back to ErrorType you get the original error value back automatically. Swift has an internal type _ObjectiveCBridgeableErrorType that has an init?(_bridgedNSError: NSError) method, but AIUI that is actually used to handle bridging of regular Cocoa NSErrors into the Swift ErrorType values that represent them (e.g. for all of the error enums found in the overlay module).

Really, the only thing we want to add to the current process is a way for an ErrorType to provide the .userInfo value of the bridged NSError, and I think we can do that just by adding a new protocol

public protocol BridgedNSError {
    var bridgedNSErrorUserInfo: [NSObject: AnyObject] { get }
}

(the property name is to avoid taking the generic term "userInfo", which the ErrorType might want to use for its own purposes)

The machinery that bridges to NSError would simply test for conformance to this protocol and use it to provide the .userInfo if present.

And of course this protocol would live in the Foundation overlay instead of being part of the stdlib proper.

CustomStringConvertible for Simple Error Messages
----------------------------------------------------------------------

I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:

  enum HollywoodCarError: Error, CustomStringConvertible {
    case exploded
    case engineFellOut
    
    var description: String {
      switch self {
      case .exploded:
        return localized("The car has exploded in a fireball.")
      case .engineFellOut:
        return localized("The car's engine has fallen out.")
      }
    }
  }

(The `CustomStringConvertible` documentation is not entirely clear on whether `description` should be user-readable and localized. I interpret it to be "yes", but if not, we can provide a `LocalizedStringConvertible` protocol along the same lines. In either case, the point is that this is *not* a feature limited to errors; there really ought to be a protocol for anything which can be shown to users in a plain-text form.)

I disagree here too. My CustomStringConvertible representation of my error may not be at all suitable for the localizedDescription of an NSError. The NSError's localizedDescription should be a string that is suitable for presenting to the user, but CustomStringConvertible isn't necessarily intended for showing to a user. For example, my enum's string representation may be "IOError(code: 3, path: "/tmp/foo")", which is certainly not what you want the user to see.

PresentableError
-----------------------

There are two other parts of NSError that I think we should widely model because they'd be useful on all platforms.

One is the ability to provide a more detailed error message. NSError calls this the `localizedRecoverySuggestion`; I recommend we just call it `suggestion` or perhaps `details`.

The other is the ability to retrieve a terse sentence fragment explaining the error's cause, which can be inserted into a larger paragraph. NSError calls this `localizedFailureReason`; I suggest simply using `reason`.

Both of these fields should be bridged into their NSError equivalents, and conforming to their protocol would also imply conformance to `CustomStringConvertible`.

I disagree here as well. If we have a way to provide the userInfo dictionary for a bridged NSError, then we don't need these fields (and in fact these fields would become useless as the bridging machinery wouldn't even look at them, seeing as how the error provides its own userInfo dictionary already). And I don't think the stdlib should try to define the semantics for how errors should be represented in a UI, since the stdlib doesn't do anything UI-related. We should leave this space open for other libraries to investigate alternatives to NSError's handling of this.

And FWIW, I don't think I've _ever_ seen anyone provide localizedRecoverySuggestion with NSError, and it's pretty rare to see a localizedFailureReason.

RecoverableError
-----------------------

Lower-priority because it's not used *that* often, but I think it's worth modeling in the long run. It may make sense to put this in Foundation instead of stdlib, though.

The idea behind Cocoa's `recoveryAttempter` is good, but I think the design is a problem, especially in Corelibs Foundation where we won't be able to perform selectors. So I suggest an alternative design. I believe it can be bridged to NSError's approach, although it would not quite be a *trivial* bridging process.

First, the RecoveryAttempting protocol, which models one particular approach to recovering from the error:

  // This is class-constrained in case `recover(from:completion:)` needs to be mutating.
  public protocol RecoveryAttempting: class, CustomStringConvertible {
    // The CustomStringConvertible name should be suitable for buttons.
    
    var destructive: Bool
    
    /// Attempt to recover from the error using this attempter. Calls `completion` when finished.
    func recover(from error: Error, completion: (Bool, ErrorType?) -> Void)
  }
  extension RecoveryAttempting {
    public var destructive: Bool { return false }
  }

Second, the RecoverableError protocol:

  public protocol RecoverableError: Error {
    var recoveryAttempters: [Any<RecoveryAttempting>] { get }
  }

We should document that you should only provide non-trivial recovery attempters (i.e. no Cancel options). Higher-level error presentation code can always do that.

Just like PresentableError, I don't think this is appropriate for the Swift stdlib.

-Kevin Ballard

···

On Sun, Mar 6, 2016, at 06:38 PM, Brent Royal-Gordon via swift-evolution wrote:


(Charles Kissinger) #11

Erica,

I wouldn’t want a single, universal error type for the standard
library. I would want to be able to selectively catch errors based on
their type rather than having to look into the “reason” string to
determine what happened. (In other words, the reason should be encoded
in the error type.)

Are you sure? People often reflexively think they want strong static
typing in their errors, forgetting that error handling of the
throw/catch variety is fundamentally dynamically typed, effectively
introspecting the error at the catch site. Think about the use cases.

Maybe we’re talking about two different things. I would typically want to be able to catch errors selectively by pattern matching based on their type, enum cases, etc.. I would not want to have to do a string match on the “reason” property in order to find out what the error was, as would be required with the Error struct that was proposed.

What approach are you suggesting?

—CK

···

On Mar 7, 2016, at 8:23 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:
on Sun Mar 06 2016, Charles Kissinger <swift-evolution@swift.org> wrote:

--
-Dave

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


(Charles Kissinger) #12

Erica,

I wouldn’t want a single, universal error type for the standard
library. I would want to be able to selectively catch errors based on
their type rather than having to look into the “reason” string to
determine what happened. (In other words, the reason should be encoded
in the error type.)

Are you sure? People often reflexively think they want strong static
typing in their errors, forgetting that error handling of the
throw/catch variety is fundamentally dynamically typed, effectively
introspecting the error at the catch site. Think about the use cases.

Oh, you’re thinking about a single “StandardLibraryError” enum with multiple cases to differentiate the errors? That’s not the kind of “universal error type" I was objecting to. I wasn’t making a distinction between catching by the enum type or the enum case, just not wanting to do it by string description.

—CK

···

On Mar 7, 2016, at 8:23 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:
on Sun Mar 06 2016, Charles Kissinger <swift-evolution@swift.org> wrote:

--
-Dave

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


(Brent Royal-Gordon) #13

I was suggesting something that was a standard fallback
error completely separate from any other modification to the error mechanism, just because it's extremely useful
to have something to throw without having to design a full error system for things like command-line utilities, etc.
In otherwords, a pre-constructed vanilla handy-error, ready for use that would have zero impact on any other error
system, implementation, or mechanism.

Ah.

If that's what you have in mind, I would give it a name like SimpleError, GenericError, or UnspecifiedError, and I would not have it carry any state other than an error message (if we introduce some standard mechanism to convey an error message, like using CustomStringConvertible). I think that, at the point where you need to pack a generic error full of random information or figure out where in the source code it came from, you probably need to start explicitly modeling the errors your code can generate.

···

--
Brent Royal-Gordon
Architechies


(Charles Kissinger) #14

I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)

I agree. A single concrete error type with stringly-typed data inside it is the precise *opposite* of how the error mechanism should be used.

Oh dear, because that is exactly *not* what I was proposing. I was suggesting something that was a standard fallback error completely separate from any other modification to the error mechanism, just because it's extremely useful to have something to throw without having to design a full error system for things like command-line utilities, etc. In otherwords, a pre-constructed vanilla handy-error, ready for use that would have zero impact on any other error system, implementation, or mechanism.

I see. You don’t want the standard library to *use* that error type, just make it available. I still worry that including it in the standard library would be a de facto sanctioning of an error-handling anti-pattern, though (i.e., errors differentiated by string descriptions).

I certainly see the value of the context information you want to provide. It would be great if that could somehow be made available to any ErrorType.

—CK

···

On Mar 6, 2016, at 7:10 PM, Erica Sadun <erica@ericasadun.com> wrote:

On Mar 6, 2016, at 7:38 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

-- Erica


(Erica Sadun) #15

The source context is super-handy. ("File "foo.swift", line 23: "Bad moon tonight"").

I included all the other stuff more or less as a mental cut-and-paste from the discussion
about how the error constants still needed to evolve (https://github.com/apple/swift-evolution/blob/master/proposals/0028-modernizing-debug-identifiers.md)

And I threw in a dictionary because who doesn't love a dictionary?

But yeah, GenericError, BasicError, SimpleError, anything like that. And you know half the time people are just going to "guard try? blah else {fatalError("oops")}"

-- E

···

On Mar 6, 2016, at 8:47 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

I was suggesting something that was a standard fallback
error completely separate from any other modification to the error mechanism, just because it's extremely useful
to have something to throw without having to design a full error system for things like command-line utilities, etc.
In otherwords, a pre-constructed vanilla handy-error, ready for use that would have zero impact on any other error
system, implementation, or mechanism.

Ah.

If that's what you have in mind, I would give it a name like SimpleError, GenericError, or UnspecifiedError, and I would not have it carry any state other than an error message (if we introduce some standard mechanism to convey an error message, like using CustomStringConvertible). I think that, at the point where you need to pack a generic error full of random information or figure out where in the source code it came from, you probably need to start explicitly modeling the errors your code can generate.

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #16

I certainly see the value of the context information you want to provide. It would be great if that could somehow be made available to any ErrorType.

—CK

The context information is a work in progress. First started discussion for this:
https://github.com/apple/swift-evolution/blob/master/proposals/0028-modernizing-debug-identifiers.md

But it got deferred, and I never got around to bring that back up. I kind of glommed the two things together.
I should probably start a separate thread for "what info should be gathered in debug identifiers".

And I would very much like to see a proposal gain momentum on another big bugaboo of mine, the
#if conditions for compilation. There was a great conversation going and it died.

-- E


(Brent Royal-Gordon) #17

`code` is still magically synthesized, as indicated by the imaginary `@_numbered` attribute. I don't like that very much, but I don't see a good alternative to it, either—RawRepresentable synthesis *almost* works, except that cases with associated values wouldn't be supported.

I disagree that this should be part of Error. `domain` and `code` are both relics of NSError integration and have no purpose when not bridging to NSError. The `domain` is really just a string representation of the error type, and the `code` is just an integral representation of the enum variant, so they're just imperfect representations of information already contained by the error value itself. We do actually need to include them in order to bridge any arbitrary Error to NSError, but they should stay as hidden implementation details (e.g. `_domain` and `_code`) like they are today. Besides, the names `domain` and `code` might be names that the concrete Error implementation actually wants to use for its own purposes.

To tell the truth, the early drafts of that email said that Error's requirements should be platform-dependent, but that the Swift compiler should always be able to synthesize all of them automatically. But as I thought about it more, I decided that probably wasn't right.

The reason I think that is Corelibs Foundation. It appears to me that the long-term plan for Swift is to have Foundation available in all environments Swift supports, except perhaps embedded ones. That means NSError is here to stay and interoperation between NSError and Swift.Error is going to be a permanent requirement.

Also, there's no need to ever "unpack" userInfo. When you bridge a Swift error into NSError, it uses a private NSError subclass _SwiftNativeNSError, which boxes up the original error, and when that NSError is bridged back to ErrorType you get the original error value back automatically. Swift has an internal type _ObjectiveCBridgeableErrorType that has an init?(_bridgedNSError: NSError) method, but AIUI that is actually used to handle bridging of regular Cocoa NSErrors into the Swift ErrorType values that represent them (e.g. for all of the error enums found in the overlay module).

Yes, and I'm suggesting we should handle bidirectional bridging with public APIs, not private magic, and in a way that can expose all of the details to both sides. If NSError is a permanent part of Swift, I think we should plan to do that sooner or later.

Really, the only thing we want to add to the current process is a way for an ErrorType to provide the .userInfo value of the bridged NSError, and I think we can do that just by adding a new protocol

public protocol BridgedNSError {
   var bridgedNSErrorUserInfo: [NSObject: AnyObject] { get }
}

(the property name is to avoid taking the generic term "userInfo", which the ErrorType might want to use for its own purposes)

The machinery that bridges to NSError would simply test for conformance to this protocol and use it to provide the .userInfo if present.

I did think about moving the userInfo stuff (both the property and the initializer) into a separate protocol, but that was late in the drafting process and the email was long enough already.

CustomStringConvertible for Simple Error Messages
----------------------------------------------------------------------

I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:

I disagree here too. My CustomStringConvertible representation of my error may not be at all suitable for the localizedDescription of an NSError. The NSError's localizedDescription should be a string that is suitable for presenting to the user, but CustomStringConvertible isn't necessarily intended for showing to a user. For example, my enum's string representation may be "IOError(code: 3, path: "/tmp/foo")", which is certainly not what you want the user to see.

Primarily, I am suggesting that we should have *a standard protocol* for "give me a textual version of this instance that I can show to a user" and that we should, by convention, use that on `Error`s to convey error messages for errors that are suitable to present to users.

I am not sure if `CustomStringConvertible` is the right protocol for that purpose, but if it isn't—if `CustomStringConvertible` is meant to provide a programmer-readable representation—then frankly I'm not sure what `CustomDebugStringConvertible` is for. But the exact protocol used is immaterial.

(To be honest, a lot of the decisions around `CustomStringConvertible` confuse me; for example, I don't understand why Swift allows you to interpolate any instance into a string rather than only `CustomStringConvertible` instances. So this might just be part of the same blind spot.)

PresentableError

I disagree here as well. If we have a way to provide the userInfo dictionary for a bridged NSError, then we don't need these fields (and in fact these fields would become useless as the bridging machinery wouldn't even look at them, seeing as how the error provides its own userInfo dictionary already).

The userInfo dictionary isn't always the source of this information; that's why NSError includes methods to retrieve them. The methods allow subclasses to override the defaults and generate error messages when they're needed, and NSError user info providers let you do the same thing without subclassing. Having bridged Swift errors call into particular methods on the Swift side would not be out of place.

And I don't think the stdlib should try to define the semantics for how errors should be represented in a UI, since the stdlib doesn't do anything UI-related. We should leave this space open for other libraries to investigate alternatives to NSError's handling of this.

It may make sense to have the protocol for this be part of Foundation, but either way, I'd *really* like to have a very easy, clean way to move this stuff between Swift.Error-land and NSError-land.

And FWIW, I don't think I've _ever_ seen anyone provide localizedRecoverySuggestion with NSError, and it's pretty rare to see a localizedFailureReason.

I think this may be why we're disagreeing about this so much—I suspect you use a very shallow subset of NSError's features, whereas I use them very aggressively. So let me explain where I'm coming from here.

In my current project, a mixed-language Mac app with about 30 errors, I have an NSError user info provider* that's about 300 lines long. Every error has a localizedDescription, most have a localizedRecoverySuggestion, and a perhaps a third to a half have a localizedFailureReason (mainly ones which end up getting wrapped in a generic "Document X cannot be opened" error; I append the localizedFailureReason to indicate what happened.) To support this, I of course have to pack a bunch of information into the `userInfo`; I have 14 keys I use for various purposes, and most of their values get inserted into error messages to make them more specific.

Structuring errors that thoroughly takes a lot of really boring code, but it pays off in really good error messages and diagnostics. I can usually pass an error straight into `-presentError:` with little to no modification.

(I don't know how you *wouldn't* use a `localizedRecoverySuggestion`, honestly; those provide the small text in an error dialog.)

* User info providers are a new feature not yet listed in the documentation, but there are doc comments for them. They basically let you specify a block that will fill in missing user info keys for a particular error domain, so you can centralize error message generation. See `-[NSError setUserInfoValueProviderForDomain:provider:]`.

RecoverableError

Just like PresentableError, I don't think this is appropriate for the Swift stdlib.

I've generally found the NSError equivalent to be a great feature marred by a fairly clumsy design. (If it had been built post-blocks it would be far better.) But you may be right that it's higher-level than what stdlib should model.

···

--
Brent Royal-Gordon
Architechies


(Dave Abrahams) #18

Please allow me to withdraw my remark; I think I may have been
experiencing a brain outage.

Thanks for your understanding.

now-back-to-our-regularly-scheduled-bikeshed-ly y'rs,

···

on Mon Mar 07 2016, Charles Kissinger <swift-evolution@swift.org> wrote:

On Mar 7, 2016, at 8:23 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

on Sun Mar 06 2016, Charles Kissinger <swift-evolution@swift.org> wrote:

Erica,

I wouldn’t want a single, universal error type for the standard
library. I would want to be able to selectively catch errors based on
their type rather than having to look into the “reason” string to
determine what happened. (In other words, the reason should be encoded
in the error type.)

Are you sure? People often reflexively think they want strong static
typing in their errors, forgetting that error handling of the
throw/catch variety is fundamentally dynamically typed, effectively
introspecting the error at the catch site. Think about the use cases.

Maybe we’re talking about two different things. I would typically want
to be able to catch errors selectively by pattern matching based on
their type, enum cases, etc.. I would not want to have to do a string
match on the “reason” property in order to find out what the error
was, as would be required with the Error struct that was proposed.

What approach are you suggesting?

--
-Dave


(Lily Ballard) #19

>> `code` is still magically synthesized, as indicated by the imaginary `@_numbered` attribute. I don't like that very much, but I don't see a good alternative to it, either—RawRepresentable synthesis *almost* works, except that cases with associated values wouldn't be supported.
>
> I disagree that this should be part of Error. `domain` and `code` are both relics of NSError integration and have no purpose when not bridging to NSError. The `domain` is really just a string representation of the error type, and the `code` is just an integral representation of the enum variant, so they're just imperfect representations of information already contained by the error value itself. We do actually need to include them in order to bridge any arbitrary Error to NSError, but they should stay as hidden implementation details (e.g. `_domain` and `_code`) like they are today. Besides, the names `domain` and `code` might be names that the concrete Error implementation actually wants to use for its own purposes.

To tell the truth, the early drafts of that email said that Error's requirements should be platform-dependent, but that the Swift compiler should always be able to synthesize all of them automatically. But as I thought about it more, I decided that probably wasn't right.

The reason I think that is Corelibs Foundation. It appears to me that the long-term plan for Swift is to have Foundation available in all environments Swift supports, except perhaps embedded ones. That means NSError is here to stay and interoperation between NSError and Swift.Error is going to be a permanent requirement.

The existence of Corelibs Foundation doesn't mean that Foundation will be something everyone is expected to use in the future. After all, it needs to be API-compatible with the real Foundation, but Foundation's API was designed for Obj-C, not Swift. A freshly-designed Foundation-free library that provides the same functionality would look rather different. Which is to say, the existence of Corelibs Foundation does not mean that the long-term plan is to keep using NSError.

> Also, there's no need to ever "unpack" userInfo. When you bridge a Swift error into NSError, it uses a private NSError subclass _SwiftNativeNSError, which boxes up the original error, and when that NSError is bridged back to ErrorType you get the original error value back automatically. Swift has an internal type _ObjectiveCBridgeableErrorType that has an init?(_bridgedNSError: NSError) method, but AIUI that is actually used to handle bridging of regular Cocoa NSErrors into the Swift ErrorType values that represent them (e.g. for all of the error enums found in the overlay module).

Yes, and I'm suggesting we should handle bidirectional bridging with public APIs, not private magic, and in a way that can expose all of the details to both sides. If NSError is a permanent part of Swift, I think we should plan to do that sooner or later.

The point is that, for error types defined in Swift, you'll _never_ have to manually unpack an NSError into the Swift error, because every NSError that represents the Swift error will have been created from the Swift error in the first place and will therefore be a _SwiftNativeNSError. The only reason to have a public API here is if you want to add the ability to construct new NSError instances in Obj-C and then convert them into your Swift error type, but I don't think that's something the stdlib needs to support.

>> CustomStringConvertible for Simple Error Messages
>> ----------------------------------------------------------------------
>>
>> I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:
>
> I disagree here too. My CustomStringConvertible representation of my error may not be at all suitable for the localizedDescription of an NSError. The NSError's localizedDescription should be a string that is suitable for presenting to the user, but CustomStringConvertible isn't necessarily intended for showing to a user. For example, my enum's string representation may be "IOError(code: 3, path: "/tmp/foo")", which is certainly not what you want the user to see.

Primarily, I am suggesting that we should have *a standard protocol* for "give me a textual version of this instance that I can show to a user" and that we should, by convention, use that on `Error`s to convey error messages for errors that are suitable to present to users.

You'd want to invent a new protocol for this purpose, then, as there's no built-in protocol that is defined such that it produces values suitable for NSError.localizedDescription. CustomStringConvertible just provides some way to produce a String from a value. It doesn't say anything about what that String is meant for. And the types of strings that you want for NSError.localizedDescription are rather unlikely to be the strings that people choose to return from CustomStringConvertible.description. localizedDescription is typically a complete grammatical sentence, whereas CustomStringConvertible usually just provides a description of the value itself

I am not sure if `CustomStringConvertible` is the right protocol for that purpose, but if it isn't—if `CustomStringConvertible` is meant to provide a programmer-readable representation—then frankly I'm not sure what `CustomDebugStringConvertible` is for. But the exact protocol used is immaterial.

CustomDebugStringConvertible is meant for printing even more verbose descriptions in the debugger or in debug logging. Many types choose to provide a reasonable amount of info in CustomStringConvertible and a much more verbose output for CustomDebugStringConvertible.

(To be honest, a lot of the decisions around `CustomStringConvertible` confuse me; for example, I don't understand why Swift allows you to interpolate any instance into a string rather than only `CustomStringConvertible` instances. So this might just be part of the same blind spot.)

Why shouldn't Swift allow you to interpolate other values? There's nothing about string interpolation that says the output is intended to be displayed in a UI, and interpolating arbitrary values is extremely useful for logging purposes.

> And FWIW, I don't think I've _ever_ seen anyone provide localizedRecoverySuggestion with NSError, and it's pretty rare to see a localizedFailureReason.

I think this may be why we're disagreeing about this so much—I suspect you use a very shallow subset of NSError's features, whereas I use them very aggressively. So let me explain where I'm coming from here.

In my current project, a mixed-language Mac app with about 30 errors, I have an NSError user info provider* that's about 300 lines long. Every error has a localizedDescription, most have a localizedRecoverySuggestion, and a perhaps a third to a half have a localizedFailureReason (mainly ones which end up getting wrapped in a generic "Document X cannot be opened" error; I append the localizedFailureReason to indicate what happened.) To support this, I of course have to pack a bunch of information into the `userInfo`; I have 14 keys I use for various purposes, and most of their values get inserted into error messages to make them more specific.

Structuring errors that thoroughly takes a lot of really boring code, but it pays off in really good error messages and diagnostics. I can usually pass an error straight into `-presentError:` with little to no modification.

(I don't know how you *wouldn't* use a `localizedRecoverySuggestion`, honestly; those provide the small text in an error dialog.)

I think this is the difference between iOS and OS X. iOS has no equivalent to -presentError:, and nobody wants to cram a lot of text into an alert. So in iOS, nobody ever bothers with -localizedRecoverySuggestion and whatnot, and -localizedFailureReason use is pretty rare.

-Kevin Ballard

···

On Sun, Mar 6, 2016, at 11:10 PM, Brent Royal-Gordon wrote:


(Ben Rimmington) #20

CustomStringConvertible for Simple Error Messages
----------------------------------------------------------------------

I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:

I disagree here too. My CustomStringConvertible representation of my error may not be at all suitable for the localizedDescription of an NSError. The NSError's localizedDescription should be a string that is suitable for presenting to the user, but CustomStringConvertible isn't necessarily intended for showing to a user. For example, my enum's string representation may be "IOError(code: 3, path: "/tmp/foo")", which is certainly not what you want the user to see.

Primarily, I am suggesting that we should have *a standard protocol* for "give me a textual version of this instance that I can show to a user" and that we should, by convention, use that on `Error`s to convey error messages for errors that are suitable to present to users.

You'd want to invent a new protocol for this purpose, then, as there's no built-in protocol that is defined such that it produces values suitable for NSError.localizedDescription. CustomStringConvertible just provides some way to produce a String from a value. It doesn't say anything about what that String is meant for. And the types of strings that you want for NSError.localizedDescription are rather unlikely to be the strings that people choose to return from CustomStringConvertible.description. localizedDescription is typically a complete grammatical sentence, whereas CustomStringConvertible usually just provides a description of the value itself

CFError and NSError also support:

kCFErrorDescriptionKey

Key to identify the description in the userInfo dictionary.

When you create a CFError object, you can provide a value for this key if you do not have localizable error strings. The description should be a complete sentence if possible, and should not contain the domain name or error code.

Available in OS X v10.5 and later.

<https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFErrorRef/>

The default localizedDescription becomes "The operation couldn’t be completed. (<domain> error <code> - <description>)".

-- Ben

···

On 7 Mar 2016, at 08:16, Kevin Ballard via swift-evolution <swift-evolution@swift.org> wrote: