Handling errors properly is essential for creating a robust and reliable applications. 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 us handle the errors properly.
This article assume you have the basic knowledge about Combine includes Publisher, Subscriber,…. You can also check my series about Combine at here
Errors types in Combine
Before we learn about handling errors strategies, it’s crucial to understands different types of errors that can occur when you using 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 cancelled 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 actually 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, you have a publisher that retrieves data from server, you can use catch
operator to catch any errors and recover by returning a default value or retrying the request
replaceError
replaceError
is seem quite the same to catch
operator. The difference is replaceError
completely ignores the error and still return a recovering value.
In the above example, we’re doing nothing than return the placeholder image in case of 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 🙌