Use RxSwift Observable Instead of Result for Failure Handling

I wanted to handle a file loading error in a consumer of an RxSwift Observable sequence. But if the sequence itself produces an .error event, it completes. After fiddling around for a while, I decided to simply use the Result enum and ended up with Observable<Result<Theme, ThemeError>>.

But someone told me about another way. A way without Result. A way of pure RxSwift.

I was intrigued. And puzzled.

Adam Borek explains how you can use materialize() on Observable sequences to get access to the underlying event that you can use instead of the Result enum. You use the .next and .error event cases instead. Great!

So my sequence turned into Observable<Event<Theme>>. What next?

You can use RxSwiftExt's convenience methods to the mix and split this sequence into two using .elements() and .errors(). That's exactly what I needed: in most cases, I map errors to a fallback value – in this case, a fallback theme the app will use. And in one instance I consume the errors to display a warning with additional info.

There's one caveat with materialize, though: the sequence will still complete after error. That means you will want to create a new sequence for every request, be it a network request or a file reading request like in my case, and then flatMap or flatMapLatest into it. That's what Adam Borek does in his code, too. If you just mapped the original observable sequence and materialize that instead, it'll complete on error.

The request produces a single value, like Observable.just, but there's no API for a throwing factory closure. I came up with the following extension:

extension Observable {
    static func attemptJust(_ f: @escaping () throws -> E) -> Observable<E> {
        return Observable.create { observer -> Disposable in
            do {
                observer.on(.next(try f()))
                observer.on(.completed)
            } catch {
                observer.on(.error(error))
            }
            return Disposables.create()
        }
    }
}

The code I use to load my themes thus becomes:

let themeURL: Observable<URL?> = // ... the user loads a file ...
let themeFromURL: Observable<Event<Theme>> = themeURL
    .filterNil()
    .flatMapLatest { url in
        return Observable
            .attemptJust { try Theme(fromURL: url) }
            .materialize()
    }

The inner, materialized sequence can error-out and I can consume the error events in the main sequence. Works like a charm.

I don't know if I like that Event has a .completed case, too. Result only knows about success and failure. It was closer to my intention. But ditching another dependency in this case is nice, too, and it taught me a thing about RxSwift, so I'll keep it and see if I understand what's going on a year from now.

Browse the blog archive