Best Strategies To Handle Errors in Combine
Handling errors properly is essential for creating a robust and reliable application. Error handling in reactive programming is comparatively more complex than its imperative counterpart. But when using Combine, you’re equipped with handy operators that can help you handle the errors properly.
This article assumes you have the basic knowledge about Combine including Publisher, Subscriber,…. You can also check my series about Combine here
Errors types in Combine
Before we learn about handling errors strategies, it’s crucial to understand different types of errors that can occur when you use Combine
Publisher errors: These errors occur when a publisher fails to produce a value due to an internal error, such as a network failure or a runtime error.
Operator errors: These errors occur when an operator in the pipeline fails to process a value due to an error condition, such as an invalid argument or a runtime error.
Subscription errors: These errors occur when a subscriber fails to receive values due to an error condition, such as a canceled subscription or a runtime error.
mapError
mapError
operator is used for mapping an error to the expected error type
import Combine
enum MyError: Error {
case testError
}
enum MappedError: Error {
case transformedError
}
let publisher = PassthroughSubject<Int, MyError>()
let cancellable = publisher
.mapError { _ in MappedError.transformedError }
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Publisher completed successfully.")
case .failure(let error):
print("Publisher completed with error: \(error)")
}
}, receiveValue: { value in
print("Received value: \(value)")
})
publisher.send(1)
publisher.send(completion: .failure(.testError))
//Outputs
//Received value: 1
//Publisher completed with error: transformedError
retry
Another common strategy for handling errors in Combine is to use the retry operator. You might want to use the retry
operator before accepting an error when working with data requests.
enum MyError: Error {
case unknown
}
let url = URL(string: "<https://example.comm>")!
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
.mapError { error -> Error in
if let urlError = error as? URLError, urlError.code == .networkConnectionLost {
return urlError
} else {
return MyError.unknown
}
}
.retry(3)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Request completed successfully.")
case .failure(let error):
print("Request failed with error: \(error)")
}
}, receiveValue: { value in
print("Received value: \(value)")
})
However, The retry
operator in Combine does not have the same functionality as the retry(when:)
operator in RxSwift. In Combine, the retry
operator simply resubscribes to the upstream publisher when an error occurs, up to a specified number of times. It does not provide a mechanism to conditionally decide whether to retry based on the error that occurred.
catch
One of the most common strategies for handling errors in Combine is to use the catch
operator. The catch operator allows you to handle errors and recover from failures in a reactive pipeline.
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.catch { error -> Just<Data> in
print("Error: \(error)")
return Just(Data()) // return a default value if an error occurs
}
For example, if you have a publisher that retrieves data from the server, you can use catch
operator to catch any errors and recover by returning a default value or retrying the request
replaceError
replaceError
seems quite the same as the catch
operator. The difference is that replaceError
completely ignores the error and still returns a recovering value.
In the above example, we’re doing nothing than returning the placeholder image in case of an error.
URLSession.shared
.dataTaskPublisher(for: URL(string: "<https://mydomain/image_654>")!)
.map { result -> UIImage in
return UIImage(data: result.data) ?? UIImage(named: "placeholder-image")!
}
.replaceError(with: UIImage(named: "placeholder-image")!)
.sink(receiveCompletion: { print("received completion: \($0)") }, receiveValue: {print("received auth: \($0)")})
Conclusion
Recognizing the importance of handling both happy and unhappy scenarios, it’s vital to discuss error management strategies in Combine. Also, you can check out the code snippet featured in this article via My Playground 🙌
Thanks for Reading! ✌️
If you have any questions or corrections, please leave a comment below or contact me via my LinkedIn account Pham Trung Huy.
Happy coding 🍻
Subscribe to my newsletter
Read articles from Phạm Trung Huy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Phạm Trung Huy
Phạm Trung Huy
👋 I am a Mobile Developer based in Vietnam. My passion lies in continuously pushing the boundaries of my skills in this dynamic field.