When writing an iOS app you are probably dealing with a lot of asynchronous code whether it is a function which takes the argument as a completion handler, notification or KVO. You might be also familiar with libraries like RxSwift or PromiseKit.
A unified, declarative API for processing values over time
Now there is a native solution — Combine. It is a brand new reactive framework from Apple which was introduced at the beginning of June during WWDC19. Combine stands on top of many asynchronous interfaces that can be found in your codebase and provides unified API for processing values over time.
Publishers, Subscribers and more
There are three key concepts: Publishers, Subscribers and Operators. Both Publisher and Subscriber protocol have two associated types. The first one is Input/Output which defines a kind of values published/received by Publisher/Subscriber. Publisher’s
receive(subscriber:) method requires Subscriber’s Input to match one’s Output type.
Second associated type is
Failure which must conform to
Error protocol. It describes type of the error that can be published/received by Publisher/Subscriber or in other words — how they can fail.
Publisher is a protocol that defines how values and errors are produced over time.
There are four kinds of messages:
- subscription — connection of a subscriber to a publisher
- value — an element in the sequence, the publisher can produce zero or more values
- error — first terminal message. Indicates that sequence ended with error
- complete — second terminal message. Indicates that publisher finished successfully
On the receiving end, there is Subscriber. They describe three events that can occur in one’s lifetime and are related to the four messages described above.
receive(subscription: Subscription)— successfully subscribed to the publisher. Called exactly once
receive(_ value: Input) -> Subscribers.Demand— Publisher can provide zero or more values to the Subscriber
receive(completion: Subscribers.Completion<Failure>)— Publisher can send exactly one completion. It can indicate successful completion or error
As you can see errors are important when handling streams of values. Dealing with them might be significant to make sure to display a correct error message or handle failed API requests.
Types of Errors
Since Publisher and Subscriber protocols add type constraints to the associated types you can use any custom type conforming to Error protocol.
As an example let’s use CurrentValueSubject which conforms to the Subject protocol which lets you publish elements with
send(_:) function. Subject protocol extends the Publisher protocol so initializing it requires providing types for both Output and Failure.
CurrentValueSubject stores all values it received and must be constructed with the initial value (as opposed to PassthroughSubject).
While working with Combine you are going to see that a lot of Publishers will describe their failure type as Never. This indicates that they can never fail or that failure has been handled earlier.
Neveris very useful for representing impossible code. Most people are familiar with it as the return type of functions like
Neveris also useful when working with generic classes.
It might be common to define Result type with Never as its Failure type to represent that something can never produce an error.
An example of this behavior in Combine can be NotifcationCenter and its new Publisher struct implementing Publisher protocol with Failure type of Never.
Error Handling in Practice
To see how different error handling operators behave in practice I’m going to use Swift Playground.
This code should give us good insight at what exactly happens when values, errors, and completions are produced.
When you run the above code you will see three emitted values and one failure. Finish won’t be called because failure is a terminal message and indicates that the publisher won’t produce any additional elements.
✅ value: initial value✅ value: 1st value✅ value: 2nd value❗️ failure: defaultSubjectError
The first error handling operator raises a fatalError when the Publisher emits an error.
_ prefix: String = "",
file: StaticString = #file,
line: UInt = #line
) -> Publishers.AssertNoFailure<Self>
To match Never type let’s add new
onNeverCompletion handler and
assertNoFailure() before attaching to sink.
Instead of error in the console, our program execution will be stopped with an error message similar to one caused by
✅ value: initial value✅ value: 1st value✅ value: 2nd value// runtime error instead of failure
That’s a situation when other error handling operators might come in handy because it’s rarely desirable to cause the runtime exception and application to stop.
One of the most useful error-handling operators is
catch which lets you replace the current publisher which caused an error with another publisher. This method defines a closure which takes error as an argument and returns Publisher.
_ handler: @escaping (Self.Failure) -> P
) -> Publishers.Catch<Self, P>
P : Publisher,
Self.Output == P.Output
In the closure, you can intercept an error and return new publisher accordingly. In this example, I’m just returning a new Just publisher. It emits a value just once and then finishes. It also does not produce any errors.
A Just publisher is also useful when replacing a value with
Now instead of error we can successfully receive a value and finish the subscription. One thing to keep in mind is that in this situation our subscription is terminated.
✅ value: initial value✅ value: 1st value✅ value: 2nd value✅ value: recovering from the error: defaultSubjectError🏁 finished
Another handy operator is
mapError which converts errors into any new error. It might be useful when fetching and converting data to give theuser the most meaningful information about what went wrong.
_ transform: @escaping (Self.Failure) -> E
) -> Publishers.MapError<Self, E>
E : Error
This code example results in a different type of error than was originally published. In most situations, you are going to use
switch case statement to match and return an appropriate error.
✅ value: initial value✅ value: 1st value✅ value: 2nd value❗️ failure: unknown
Another operator that might be helpful in some situations is
setFailureType . By looking at its declaration in the documentation one thing you are going to notice is that it is declared as Publisher’s extension with a
where clause indicating that it can only be used when Publisher never fails(
Self.Failure == Never).
to failureType: E.Type
) -> Publishers.SetFailureType<Self, E>
E : Error
When you try calling it on publisher with Error not being Never you will see this compiler error message:
❗️ Referencing instance method 'setFailureType(to:)' on 'Publisher' requires the types 'Error' and 'Never' be equivalent
The easiest way to simulate this behavior is to instantiate subject with Never or use one we’ve got already declared and call
assertNoFailure . Now it is possible to set failure type to match the completion.
✅ value: initial value✅ value: 1st value✅ value: 2nd value// runtime error instead of failure
This code will result in runtime exception because that’s a default way of handling errors when they are declared as
Never . Additionally this method might be also used on
Justpublisher to match completion handler which doesn’t expect
Last but not least there is
retry. It takes one argument which specifies the number of retries that going to happen in cases of error. After number of retries is exceeded the publisher will pass the error to the subscriber.
_ retries: Int
) -> Publishers.Retry<Self>
retry is very useful when working with
URLSession . Instead of resuming data tasks recursively when the server returned an error it is much easier to just call
retry and restart the chain of Combine operators.
One of the most basic errors handling operators is
replaceError which simply replaces an error with provided value and finishes normally.
with output: Self.Output
) -> Publishers.ReplaceError<Self>
In this example error will be replaced with provided the String.
✅ value: initial value✅ value: 1st value✅ value: 2nd value✅ value: replaced error🏁 finished
Practical example with URLSession
One of the most common examples of dealing with errors is making URL requests where error can arise due to the internet connection, server problems or decoding.
URLSession now comes with functions that return
URLSession.DataTaskPublisher . It’s a new struct that conforms to Publisher protocol. Its Output is a tuple containing
URLResponse while Failure is
for url: URL
) -> URLSession.DataTaskPublisherfunc dataTaskPublisher(
for request: URLRequest
) -> URLSession.DataTaskPublisher
Decoding data to user-defined type can be achieved with a new
decode function which takes expected type and decoder as arguments.
func decode<Item, Coder>(
) -> Publishers.Decode<Self, Item, Coder>
Item : Decodable,
Coder : TopLevelDecoder,
Self.Output == Coder.Input
Combining these operators with previously described ones (map, mapError etc.) lets you chain them all together which results in more linear and shorter code.
The above snippet presents an example of dealing with data decoding and error matching to give a user more meaningful message about what went wrong, whether it’s a problem with the internet connection or an internal problem with data decoding.
eraseToAnyPublisher at the end of the chain of operators is used to hide details of the operators that were used. If it was not used here, return type of this expression would look like this:
Publishers.Retry<Publishers.MapError<Publishers.Decode<Publishers.Map<URLSession.DataTaskPublisher, JSONDecoder.Input>, Item, JSONDecoder>, ServiceError>>
Errors in Combine are passed downstream and when the first error occurs all following operators (except ones dealing with errors) will be skipped. In our example once there is a connection problem no other operator than
retry is called.
In the picture below you can see all the operations with their corresponding Output and Failure types. Green color marks operations which primarily deal with dealing with input/output and red color applies to operators which deal with errors (mapping, retrying failed subscription).
Where to go from here…
Combine framework will become more common among iOS developers when SwiftUI becomes a standard (official release in September 2019).
Even if you don’t start using SwiftUI in your apps, Combine has a lot to offer. The only requirement is that your app’s target is set to at least iOS 13.0. There are a lot of new features that make common tasks like handling user’s input or making URL request easier with Publishers. Also, there are a lot of different ways of dealing with error and understanding them. I hope you will find this article helpful and use Combine’s error handling features in your projects.
All of the code snippets used in this article can be found in the following repo: