Error handling in MVVM architecture in Swift
If you care about user experience, error handling is a big part you have to cover. We can design how an mobile app looks like when it works, but what happen when something goes wrong. Should we display an alert to the user? Can the error stay silent? And mostly how to implement it the best way with your current design pattern? Let’s see our options while following MVVM pattern.
As prerequisite to this post, you need to be familiar with MVVM design pattern. If you’re not, don’t worry, I created a guid to show you how to implement it in Swift.
From my previous example, I have a View (here a UIViewController) asking its ViewModel to fetch currencies from server side.
class CurrencyViewController: UIViewController {
var viewModel = CurrencyViewModel()
override func viewDidLoad() {
super.viewDidLoad()
... // more code here
self.viewModel.fetchCurrencies()
}
}
struct CurrencyViewModel {
func fetchCurrencies(_ completion: ((Result<Bool, ErrorResult>) -> Void)? = nil) {
guard let service = service else {
completion?(Result.failure(ErrorResult.custom(string: "Missing service")))
return
}
service.fetchConverter { result in
DispatchQueue.main.async {
switch result {
case .success(let converter) :
// reload data
self.dataSource?.data.value = converter.rates
completion?(Result.success(true))
break
case .failure(let error) :
print("Parser error \(error)")
completion?(Result.failure(error))
break
}
}
}
}
}
As you can see, the challenge here around error handling is not when to forward the result to the view, but which way is the best one.
Closure as parameter
My first option was to give a completion handler once we fetch the data.
// in CurrencyViewController
self.viewModel.fetchCurrencies { result in
switch result {
case .failure(let error): // Display here
case .success(_): // Nothing to do
}
}
What I don’t like about that solution is the logic is at both level. I have a switch case
of my result in the ViewModel (getting the data, getting the error) but also in my View (getting the success of the request, getting the result).
One of the key in MVVM design pattern is the separation of concern, here it doesn’t look that well separated and it’s a bit redundant.
Dynamic value
Another way would be to bind the error state of the ViewModel to the View itself. It’s what we do to refresh the view when we have the data, why not doing the same if we have an error?
struct CurrencyViewModel {
var error : DynamicValue<ErrorResult?> = DynamicValue<ErrorResult?>(nil)
func fetchCurrencies(_ completion: ((Result<Bool, ErrorResult>) -> Void)? = nil) {
... // later in the code
switch result {
case .success(let converter) :
// reload data
self.dataSource?.data.value = converter.rates
case .failure(let error) :
self.error.value = error
}
}
}
class CurrencyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
...
self.viewModel.error.addObserver(self) { error in
// display error
}
self.viewModel.fetchCurrencies()
}
}
This one looks already better. I added a dynamic ErrorResult
in my ViewModel that I observed from my View to eventually display something to the user. However we can’t control how many time this can be triggered, let’s see what else we can try.
Closure as property
Finally, an alternative to a dynamic value that we have to observe, we can still use a closure as property.
struct CurrencyViewModel {
var onErrorHandling : ((ErrorResult) -> Void)?
func fetchCurrencies(_ completion: ((Result<Bool, ErrorResult>) -> Void)? = nil) {
... // later in the code
switch result {
case .success(let converter) :
// reload data
self.dataSource?.data.value = converter.rates
case .failure(let error) :
self.onErrorHandling?(error)
}
}
}
class CurrencyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
...
self.viewModel.onErrorHandling = { error in
// display error
}
self.viewModel.fetchCurrencies()
}
}
This is slightly better that the observable one in that sense the ViewModel stays in control of what is going to happen to the View, when it’s triggered. If you want to keep an error silent and don’t display anything, you just need to not trigger the closure. We can’t really do that with the previous one because we don’t know what other class is observing the value.
However, as always in development, there is no perfect solution. If you have multiple type of error (parsing data failed? network request failed? etc) and if you have multiple methods that can trigger an error, it’s hard to keep everything under one handler.
Eventually you will also have to add extra logic in the ViewModel to filter which error can trigger the closure or from which method.
In conclusion, we’ve seen multiple solutions to tackle error handling in Swift with an MVVM pattern. We have to keep in mind that the solution has to be easy to test and to maintain, for instance dynamic value and closure are easy to unit test. That is why the separation of concern is so important, and that will help you keep a clean codebase and deliver the best user experience.
You can find my final solution on Github.