Error handling with Combine
If there is one critical state that you must handle intelligently in your application, it is the error state. Error states put your user journey in a potential stalemate and your users must be aware of what has happened to the app.
Error handling in reactive programming is comparatively more complex than its imperative counterpart. Thankfully, Combine comes equipped with handy operators that would help us manage our error events elegantly.
This blog assumes you have a basic understanding of Combine fundamentals.
setFailureType
Sometimes you might have to turn infallible publishers into a failable ones. Infallible publishers are those that have a failure type as Never
.
Just("").sink(receiveValue: ((String) -> Void))
Just
is a publisher that only emits the specified output just once. It is an infallible Publisher.
At first instance, you might be wondering why is this even needed, because if my upstream publisher has no errors, why should I set a failure type. A commonly encountered instance is during publisher chaining. Some APIs might need you to have a fallible publisher such as the iOS 13 version of flatMap
.
tryMap
While publisher chaining, we might need to have custom logic that could throw errors. A map
operator allows only the manipulation of the emitted values of a publisher, whereas tryMap
gives us the additional flexibility to throw errors if we encounter them.
Consider you are receiving a list of temperatures for a running motor from a monitoring system. You would like to calculate how less it is from the threshold temperature and throw an error whenever it is beyond the threshold to avoid any mishaps.
- We declared a custom error to hold different types of errors that can come up.
- We declared a
thresholdTemperature
and a PassthroughSubject that will supply us with the temperatures. PassthroughSubjects are also Publishers underneath and you can observe their events. - We put a
tryMap
to calculate and return the difference from the threshold. Also, we threw an error if in case the temperature is more than the threshold.
Combine also provides try
support for different operators, thereby allowing you to throw errors while using these operators.
mapError
Many a time, we would want to map the error object received from the upstream to a different error type downstream. A general use case for this could come up during API calls and chaining.
Let us consider a situation where you are making a network call to get data through URLSession
.
- We declared a custom error enum that has a static method to convert any
Swift.Error
to our custom error type. For the time being, we are passing.internetNotFound
as the error. - We called
dataTaskPublisher(for:)
to provide the data for our URL. The method returns us aURLSession.DataTaskPublisher
which hasURLError
as its failure type. - We call
mapError
to map theURLError
to our app error.
While implementing the previous temperature control example in Playground, you would have noticed the auto-completion block for sink
temperatureListSubject
.tryMap {
.....
throw TemperatureError.thresholdReached
}
.sink(receiveCompletion: <((Subscribers.Completion<Error>) -> Void), receiveValue: <((Int) -> Void)>)
The receiveCompletion
block has error
type as Swift.Error
. You might find it a bit surprising as to how the error that we have thrown has been replaced as a normal Swft.Error
. This happens because tryMap
type erases the error of an upstream publisher to Swift.Error
. So in order to have the desired error type, we have to perform a mapError
post the tryMap
.
temperatureListSubject
.tryMap {
.....
throw TemperatureError.thresholdReached
}
.mapError { error in error as? TemperatureError ?? TemperatureError.unidentified }
catch
We can catch any error in a Publisher upstream and handle by passing another publisher downstream.
- We declared a function that triggers a fallback API and returns a publisher with the desired output.
- We used
catch
operator to catch the upstream errors and triggered a fallback API to give a chance for recovering from the failure.
catch
expects the upstream output to be the same as downstream output i.e the output dataTaskPublisher
should be the same as the triggerFallbackAPI
.
retry
Combine provides us with a mechanism to retry our failed publishers. This helps us to write clean code for any retry mechanism avoiding complex logic.
An example for retrying could be fetching an authentication token from the server. Authentication tokens are essential for validating your requests on the server-side. And hence, failures in these APIs should be guaranteed with a configurable number of retries.
We retried our get authentication token API with 2 retries. The retry will ensure that it re-subscribes to the upstream when there is a failure and trigger the API call.
replaceError(with:)
We come across many workflows wherein we have to fall back to a default value in case there is an error. replaceError(with: )
maps any error upstream with a default value. We need to make sure that the value we replace should be of the same type as Output
of the upstream Publisher.
While fetching an image from a URL, we might encounter a failure and we need to fall back to a placeholder image. This is easy to handle at the call site in the error block, but using a replaceError
reduces the overhead at the call site and moves it in the publisher chaining.
- We mapped the result to a
UIImage
object. - If we receive any error, we replace the same with a placeholder image.
Brainteaser
Could you guess what will be the failure type after you have replaced the error using replaceError(with:)
? It would be of type Never
since all your errors are mapped to a concrete Output
.
I would love to hear from you
You can reach me through the following channels: