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