Error Handling in Swift Combine Framework
Errors are inevitable part of our programming process, and the Swift Combine Framework is not an exception. While we cannot completely eliminate errors, we should focus our efforts on different ways of recovering from them. In present article let’s study Combine’s way of handling errors.
Basic understanding of Combine is recommended for this article. I suggest reading the bird-eye overview of the Combine framework before you proceed.
Error Handling Strategies
Error handling is the process of responding to and recovering from error conditions in your app [1].
Once an error occurs, the two things we can do about it is to terminate or recover. Let’s see how we can achieve this with the Swift Combine Framework.
Terminating the App
The most straightforward solution is not to handle errors at all. Although I recommend against shipping such code to real users, this strategy may be useful for development. While debugging, we often want to crash as early and as loudly as possible. This makes it easy to track down and fix errors in our Swift code. Here are two more scenarios when want not to worry about error handling:
- When writing experimental code.
- When teaching or studying.
Combine provides the assertNoFailure(_:file:line:)
method for this purpose. It terminates an app in case previous publisher sends an error. It will also print debugging information to help you track down the issue.
💡 If you are sure that an error can never occur in your stream, set publisher’s
Failure
type toNever
. It disables errors propagation on syntax level. It is much safer thanassertNoFailure(_:file:line:)
, that can potentially crash your app.
Fallback Strategies
It’s often too extreme to finish Combine stream with a failure, after which no more events can be processed. In some cases errors are expected to happen. It can be that you are searching for a non-existent term, having an unstable network connection or mistyping a phone number in a text field. As a rule of thumb, when an error is expected and not fatal to the stream, we want to recover from it. The strategies for doing this are: catching, replacing, retrying and skipping. Let’s discuss each one in detail.
Catching and Replacing Errors
The catch(_:)
method provides a way to replace failed publisher with another publisher. Typically, you’ll use catching to replace an error with some kind of placeholder data, such as:
- Data loading error with an empty data set.
- Image that failed to load with a placeholder image.
- Username that failed to load with “unknown” placeholder.
replaceError(with:)
acts similarly to the catch(_:)
method, except it replaces an error with an element, instead of creating new publisher. The difference is best demonstrated with a code snippet:
struct DummyError: Error {}
// 1
Just(1)
.tryMap { _ in throw DummyError() }
.catch { _ in Just(2) }
.sink { print($0) }
// 2
Just(1)
.tryMap { _ -> Int in throw DummyError() }
.replaceError(with: 2)
.sink { print($0) }
Both streams print 2
, but achieve this differently:
catch
replace current publisher with entirely new publisher with a value2
.replaceError
substitutes an error with a single value2
.
Retrying Errors
Retry gives your subscription a second chance by re-creating it for a given number of attempts. After all attempts are exhausted, retry(_:)
propagates an error downstream.
💡
catch
attempts to fix an error downstream, whileretry
does this upstream.
It’s common to retry publishers that we hope will complete without an error. Some examples are: network requests, user input fields that have some sort of validation.
let url = URL(string: "https://www.vadimbulavin.com")
URLSession.shared.dataTaskPublisher(for: url!)
.retry(3) // Retry network request up to 3 times
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print($0) },
receiveValue: { _ in })
Mapping Errors
Infrastructure layer is the most common source of errors (database, network etc.). However, these errors are too low-level and need to be translated into the higher-level language your app speaks.
In Layered Architecture we discuss why it’s important to separate your Swift app into layers, explain layers design and communication between them.
To achieve this we need a way to transform low-level errors, such as the HTTP 404 Not Found Error, into the ones that make sense for the app in its current state. Swift Combine Framework provides the mapError(_:)
operator for this purpose.
// 1
enum APIError: Error {
case userIsOffline
case somethingWentWrong
}
// 2
let errorPublisher = Result<Int, Error>.Publisher(URLError(.notConnectedToInternet))
// 3
errorPublisher
.mapError { error -> APIError in
switch error {
case URLError.notConnectedToInternet:
return .userIsOffline
default:
return .somethingWentWrong
}
}
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
- Declare custom error type.
- Create
Result
publisher that fails withURLError
. - Subscribe to that publisher, transforming generic
Error
intoAPIError
.
Similarly to
Just
, theResult
publisher emits single element and then completes. The difference is thatResult
may complete with either success or failure, butJust
always succeeds.
It will print:
failure(... APIError.userIsOffline)
Raising Errors
Combine has a family of try
-prefixed operators, which accept an error-throwing closure and return a new publisher. The publisher terminates with a failure in case an error is thrown inside a closure. Here are some of such methods: tryMap(_:)
, tryFilter(_:)
, tryScan(_:_:)
, you see the pattern here. There are 12 try
-operators as of Xcode 11.1.
Summary
Let’s recap what we’ve just discussed about error handling with Combine. Here is what we can do in case of error:
- Terminate an app with
assertNoFailure(_:file:line:)
. This may be useful for development and debugging. - Replace failed publisher with a value by means of
replaceError(with:)
. - Replace failed publisher with a completely new publisher using
catch(_:)
. - Give your subscription a second chance with
retry(_:)
.
You can transform an error into another error with mapError(_:)
.
Further reading
If you want to learn more about the Swift Combine Framework, I have some articles for you:
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.