Error handling in Combine explained with code examples

Once you get started with Combine you’ll quickly run into error handling issues. Each Combine stream receives either a value or an error and unlike with frameworks like RxSwift you need to be specific about the expected error type.

To be prepared on those cases I’ll go over the options available in Combine to catch, ignore and handle errors on a stream. Besides that, some important things you need to know when an error occurs on your stream.

Just getting started with Combine? You might want to first take a look at Getting started with the Combine framework in Swift or my Combine Playground.

Combine streams and typed errors

A big difference between a framework like RxSwift and Combine is the requirement of typed error definitions in streams. If we compare the Observable with its Combine equivalent AnyPublisher we can see the difference in the type declaration.

public class Observable<Element> : ObservableType
struct AnyPublisher<Output, Failure> where Failure : Error

The AnyPublisher requires us to specify the Failure error type while the Observable only takes the generic Element type.

Swift requires us to think about error handling which we can take as something good. However, it does not hold us back from defining the expected type as just Swift.Error which basically comes down to the same behavior as in RxSwift.

Once you do require your stream to expect a certain error type you’ll run into casting errors as each operator needs to return the same error type as the leading stream. Let’s dive into the Combine operators for error handling.

Mapping errors using mapError

To map an error to the expected error type we can use the mapError operator. In the following example, we have a passthrough subject which expects a URL output and a RequestError error type.

enum RequestError: Error {
    case sessionError(error: Error)
}

let imageURLPublisher = PassthroughSubject<URL, RequestError>()

Once we start mapping this stream into a URLSessionDataTaskPublisher we immediately get an error pointing out the error type mismatch.

Error handling in Swift Combine using mapError
Error handling in Swift Combine using mapError

In this case, the solution is as simple as using the mapError operator which will wrap the URLError into a RequestError using the session error case we defined earlier.

let cancellable = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
        .mapError { error -> RequestError in
            return RequestError.sessionError(error: error)
        }
}.sink(receiveCompletion: { (error) in
    print("Image request failed: \(String(describing: error))")
}, receiveValue: { (result) in
    let image = UIImage(data: result.data)
})

// Fetches the image successfully.
imageURLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!)

// Prints: Image request failed: RequestError.sessionError(error: Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found."
imageURLPublisher.send(URL(string: "https://unknown.url/image")!)

Using the retry operator

In the above example, we’ve used a URLSessionDataTaskPublisher. You might want to use the retry operator before actually accepting an error when working with data requests. It takes the number of retries to take before letting the stream actually fail.

Catching errors

If you want to catch errors early and ignore them after you can use the catch operator. This operator allows you to return a default value for if the request failed. Examples of this could be:

  • An empty array for search results
  • A default image placeholder if the image request failed

The latter is the one we will use in our example.

let notFoundImage: UIImage? = UIImage()
let imageURLPublisher = PassthroughSubject<URL, RequestError>()
let cancellable = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
        .mapError { error -> RequestError in
            return RequestError.sessionError(error: error)
        }
}.map({ (result) -> UIImage? in
    return UIImage(data: result.data)
}).catch({ (error) -> Just<UIImage?> in
    return Just(notFoundImage)
}).sink(receiveValue: { (image) in
    _ = image
})

imageURLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!)

Using replaceError instead of catch

ReplaceError vs Catch: both operators seem quite the same. The big difference is that the replaceError(:) operator is completely ignoring the error. As in the above example, we’re doing nothing more than returning the placeholder notFoundImage in the case of an error.

We could simplify this by using the replace error operator which will directly map any errors into our placeholder image:

let imageView = UIImageView()
let notFoundImage: UIImage? = UIImage()
let imageURLPublisher = PassthroughSubject<URL, RequestError>()
let cancellable = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
        .mapError { error -> RequestError in
            return RequestError.sessionError(error: error)
        }
    }.map { (result) -> UIImage? in
        return UIImage(data: result.data)
    }
    .replaceError(with: notFoundImage)
    .assign(to: \.image, on: imageView)

When the assign(to:on:) operator is unavailable

A common example in which you’ll need to map errors is when you try to assign an outcoming value to a property of an object. You’ll try to use the autocompletion and you find out that the assign(to:on:) operator is unavailable. The following error will occur if you force to write the code either way:

Referencing instance method ‘assign(to:on:)’ on ‘Publisher’ requires the types ‘RequestError’ and ‘Never’ be equivalent

You can fix this by either catching the error as in explained in the above example or by simply using the assertNoFailure operator. This operator will raise a fatalError and should, therefore, only be used if it’s a programming error. If an error is expected you should always use the catch operator instead.

Conclusion

We’ve covered a lot about error handling in Combine which should be enough to make you beat all those failing cases! Make sure to handle errors accordingly and do not simply ignore them. The unhappy flow is just as important to your users as a happy flow.

If you’d like to play around with the things you just have learned, take a look at my Swift Combine Playground which includes a page about error handling in Combine.

To read more about Swift Combine, take a look at my other Combine blog posts: