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 🍻

10
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.