Error handling with Combine

G. Abhisek
4 min readApr 1, 2022

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.

  1. We declared a custom error to hold different types of errors that can come up.
  2. 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.
  3. 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.

  1. 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.
  2. We called dataTaskPublisher(for:) to provide the data for our URL. The method returns us a URLSession.DataTaskPublisher which has URLError as its failure type.
  3. We call mapError to map the URLError 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.

  1. We declared a function that triggers a fallback API and returns a publisher with the desired output.
  2. 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.

  1. We mapped the result to a UIImage object.
  2. 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:

Twitter — @gabhisek_dev

LinkedIn

--

--