I'm looking for some suggestions for propagating / converting errors at different levels of the application.
Might a bit of a contrived example... but let's if I manage to convey it:
Let's say I have an app that fetches some data and displays it on the screen. At several places along the process here I might end up with errors specific to the operations performed:
networking errors
JSON parsing errors
model logic errors
My networking client might define its errors as
enum NetworkError: Error {
case timeout, etc. etc.
}
My json parsing might have its own errors as well:
enum JSONError: Error {
case missingKey
}
my model might say:
enum ValidationError: Error {
case logicError
}
I might then have a function that fetches the data (potential NetworkError), parses it (potential JSONError) and validates it (ValidationError).
Is the right way to model this is as one error type?
enum APIError {
case timeout,
case jsonMissingKey
case logicError
}
In this solution, the user of has to handle all these errors. And it feels strange because they the error cases don't feel like the belong grouped as one type.
maybe group them?
enum APIError {
case netowrk(NetworkError)
case json(JSONError)
case validation(ValidationError)
}
What are some solutions to modeling this in a way where I don't lose the actual error between layers, but maybe potentially avoid exposing all of them to the user?
As long as you expose your error handling as using Error instances, and implement appropriate descriptions, users don't need to know what error types you actually produce until they need to take particular action based on the particular error.
In Alamofire we encapsulate all of our internal errors using the AFError type, which has a variety of underlying types to separate different types of errors. However, all of our errors are exposed as Error, both so users don't have to care about AFError but also because we return underlying system errors directly. We have good (enough) descriptions for all of our errors so users can see what when wrong just by printing the error without having to know what type it is. If they do want to take some particular action we offer the asAFError: AFError? extension on Error so that they can pull that value out more easily. We also offer a variety of Bool properties to make it easy to check for a precise type of error.
I sometimes use the last pattern you mentioned, but only when I expect client code to want to catch and handle errors, and I have documented exactly which errors to expect:
enum ExpectedErrors {
case underlyingA(UnderlyingA)
case underlyingB(UnderlyingB)
}
When using that pattern, I often also return a Result intead of using throws, since the possible errors are then selfâdocumented by the API itself. (Clients can still tag .get() on the end to use it as if it were a method that throws.)
But in the vast majority of casesâand especially if the lower API makes no guarantees about which errors it throwsâthen I donât bother wrapping them for two reasons:
The underlying errors usually explain themselves for the most part. (Though I make sure the localizedDescription is reasonable on any new error types I create myself.)
Catching code with as becomes more confusing if the same error might be nested inside other ones:
do {
something()
} catch let error as NoMemoryError {
// Oops. Missed it because it was actually a
// NetworkingError.noMemory(NoMemoryError).
emptyCache()
tryAgain()
} catch {
fatalError("Cannot recover.")
}
In these cases I just try and let the errors propagate themselves. (Or reâthrow them unaltered from a catch statement.)