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.
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:
- @Published risks and usage explained with code examples
- RunLoop.main vs DispatchQueue.main: The differences explained
- PassthroughSubject vs. CurrentValueSubject explained
- How to observe NSManagedObject changes in Core Data using Combine
- Getting started with the Combine framework in Swift
- Error handling in Combine explained with code examples
- Combine debugging using operators in Swift
- Creating a custom Combine Publisher to extend UIKit