Struct vs Enum for Error Types

I recall a discussion (which I cannot find) a while back discussing the tradeoffs of using a struct with a nested "code" enum vs using an enum with associated values as an Error type where the conclusion was the struct approach was generally preferred.

What are the tradeoffs between the two approaches? IIRC the primary concern was around ABI stability

There is no practical difference from an ABI stability standpoint. You can add stored properties to structs or enum cases to enums by default, unless either type is @frozen. However, keep in mind that you cannot change a struct into an enum, or vice versa, without breaking ABI.

The struct approach might be best if you have some information that is common to all error codes. For example:

// Syntax error in an imaginary programming language
struct ParseError: Error {
  let file: String
  let line: Int
  let column: Int
  let code: Code
  enum Code {
    case unexpectedToken(Character)
    case unknownFunction(String)
    case unterminatedStringLiteral
  }
}

Otherwise I'd probably go with an enum.

4 Likes

One big disadvantage to using enums for errors is that if you're developing standard built-from-source packages (i.e., not using library evolution and distributing something as a prebuilt library), adding a new case to the enum is a source-breaking change for your clients because they could theoretically be switching exhaustively over them. So instead, you have to introduce new separate enums to represent new failure modes in the future. This limitation is one of the motivations behind pitches like this one.

5 Likes

Here is a code snippet from Curt along with some more discussion about public enum types and the potential gotchas.

Is this for an app or for a framework/library that could will be used by third party?

Enums seem more powerful in some regards (switching).
Structs are more powerful in other regards (ease of adding extra fields).

One extra caveat with structs - those won't give nice error codes automatically when converted to NSErrors, you'd need to provide your code for that.

Even once we fix the extensibility of enums which will allow you to add new cases I personally still recommend using structs over enums. From experience you often want to add associated data to errors when using enums you can add that as associated values to a case but adding new associated values to an existing case is an API breaking change. With structs you can freely add more properties to it and extend it further.

Now there is still benefit of providing something that allows exhaustive matching which can only be done by using an enum. So a common pattern that we have used in our server libraries looks like this

struct MyError: Error {
  enum Code {
    case foo
    case bar
  }
  var code: Code
  var someAdditionalInfo: String
  var someMoreInfo: Int
}

This enables you to evolve your error type since it is a struct but provides users the possibility to exhaustively match over the codes. (This assumes the proposal that @allevato linked above is getting accepted before that you would need to model Code as a struct with static lets to achieve extensibility)

2 Likes

In theory, modeling that as an enum with the same associated structure for every case would be better…

enum MyErrorEnum: Error {
  struct Info {
    var someAdditionalInfo: String
    var someMoreInfo: Int
  }
  case foo(Info), bar(Info)
}
do throws(MyErrorEnum) {
  throw .foo(.init(someAdditionalInfo: "", someMoreInfo: 0))
}
catch .foo { }
catch .bar { }

…but exhaustive checking with typed throws is broken.

So in practice, it has to be handled virtually the same. :frowning:

do throws(MyError) {
  throw .init(code: .bar, someAdditionalInfo: "", someMoreInfo: 0)
} catch {
  switch error.code {
  case .foo: break
  case .bar: break
  }
}

do throws(MyErrorEnum) {
  throw .bar(.init(someAdditionalInfo: "", someMoreInfo: 0))
} catch {
  switch error {
  case .foo(let info): break
  case .bar(let info): break
  }
}