How I’ve always imagined this feature is more or less syntactic sugar that the compiler uses to create a sort of enum-like structure under the hood. This is more or less how I envision it:
// Error type declaration
//
// The keyword `errortype` would function very comparably to `typealias` with the addition that the compiler typechecks the "unioned" types to ensure that they're error types and that the types aren't circular (i.e. `errortype ErrorA = ErrorB | ...`, `errortype ErrorB = ErrorA | ...`).
errortype NetworkDecodingError = NetworkError | DecodingError
This is valid for and can be used in the same contexts as type aliases (e.g. globally, to satisfy a protocol associated type requirement, etc.). The declared error type can be implicitly used anywhere any Error is accepted, either as an existential or for constrained types.
Under the hood, the compiler would create an manage an “enum-like” structure that would look very comparable to the following:
enum NetworkDecodingError: Error {
case network(NetworkError)
case decoding(DecodingError)
}
This makes the declared type a more materially concrete type that Swift can pass around to avoid the overhead created by existential types (unless of course one of the nested error types is itself an existential, which might not actually be allowed as you could create an implicit circular error type by obscuring the circular references indirectly through a protocol).
Since this error type is usable anywhere any Error is accepted, we would work this into typed throws modifiers:
func loadData<T>(from url: URL, ofType: T.Type) async throws(NetworkDecodingError) -> T where T: Decodable {
...
}
Then when we go to use it in a do-catch statement, it might look something like the following:
do {
let decodedType = try await loadData(from: url, ofType: T.self)
} catch let error as NetworkError {
// handle networking specific error
} catch let error as DecodingError {
// handle decoding specific error
}
Under the hood Swift would do all of the “unpacking” necessary to do the matching and would look something like the following:
do {
let decodedType = try await loadData(from: url, ofType: T.self)
} catch let NetworkDecodingError.network(error) {
// handle networking specific error
} catch let NetworkDecodingError.decoding(error) {
// handle decoding specific error
}
Like enums, we could also allow for the @frozen attribute to be applied to errortype declarations that would allow the compiler to ensure that the user handles potential future error types that might get added to the error type if the @frozen attribute isn’t used on the declaration:
// Library A:
public errortype NetworkDecodingError = NetworkError | DecodingError
public func loadData<T>(from url: URL, ofType: T.Type) async throws(NetworkDecodingError) -> T where T: Decodable
// Library B:
do {
let decodedType = try await loadData(from: url, ofType: T.self)
} catch let NetworkDecodingError.network(error) {
// handle networking specific error
} catch let NetworkDecodingError.decoding(error) {
// handle decoding specific error
}
// error: 'Errors thrown from here are not handled because the enclosing catch is not exhaustive'
// note: 'handle unknown errors using "catch let @unknown error"'
In essence, this would be more or less a syntactic wrapper around enums to support this feature. Would love some feedback on this approach for supporting this feature.