0

Best strategies to handle errors in Combine

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 🙌


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí