Search code examples
iosswifterror-handlingrx-swiftflatmap

How do I recover from a failed rx chain?


Rx seem a bit fragile in that it closes down an entire chain if one single thing doesn't work. That has become a real problem in my code as I have a chain that requests parameters through ble. First we ask for ids, then definitions which is sort of mapping min and max values, lastly it asks for the actual parameters:

override func getParameters() -> Single<[ParameterModel?]> {
    parameterCounter = 0
    parameterDefinitionCounter = 0
    
    return getParamterIds().do(onSuccess: { [weak self] values in
        self?.numberOfParameterIds = Float(values?.count ?? 0)
    })
        .flatMap { ids in
            Single.zip(ids!.compactMap { self.getParamterDefinition(id: $0) }) }
        .flatMap { parameters in
            Single.zip(parameters.compactMap { self.getParameter(id: $0!.id) }) }
}

So if we get an array with 30 parameter ids, it goes into getParamterDefinition(id: $0). And if it fails on a single one of those, which it does, the whole thing closes down and self.getParameter(id: $0!.id) is never run. So even though 29 parameters pass through getParamterDefinition(id: $0) nothing is passed to self.getParameter(id: $0!.id).

How do I recover from an error and keep going in the chain so that those that were successful in getParamterDefinition(id: $0) gets passed to self.getParameter(id: $0!.id) and gets displayed to the user?

*** UPDATE *** This is the final result for anyone interested in solving issues like these:

override func getParameters() -> Single<[ParameterModel?]> {
        parameterCounter = 0
        parameterDefinitionCounter = 0
        
        func getFailedParameter(id: Int) -> ParameterModel {
            return ParameterModel(id: id, name: String(format: "tech_app_failed_getting_parameter".localized(), "\(id)"), min: 2000,
                                  max: 21600000, defaultValue: 2500, value: 2500,
                                  unit: "ms", access: 0, freezeFlag: 0,
                                  multiplicator: 1, operatorByte: 1, brand: 0,
                                  states: nil, didFailRetrievingParameter: true)
        }
        
        return getParamterIds().do(onSuccess: { [weak self] values in
            self?.numberOfParameterIds = Float(values?.count ?? 0)
        }).catchError { _ in return Single.just([]) }
            .flatMap { ids in
                Single.zip(ids!.compactMap { id in
                    self.getParamterDefinition(id: id).catchError { [weak self] _ in
                        self?.parameterErrorStatusRelay.accept(String(format: "tech_app_parameter_definition_error_status".localized(), "\(id)"))
                        return Single.just(getFailedParameter(id: id))
                    }
                })
            }
            .flatMap { parameters in
                Single.zip(parameters.compactMap { parameter in
                    guard let parameter = parameter, !(parameter.didFailRetrievingParameter) else {
                        return Single.just(parameter)
                    }
                    
                    return self.getParameter(id: parameter.id).catchError { [weak self] _ in
                        self?.parameterErrorStatusRelay.accept(String(format: "tech_app_parameter_error_status".localized(), "\(parameter.id)"))
                        return Single.just(getFailedParameter(id: parameter.id))
                    }
                })
            }
    }

Solution

  • You should use the Catch methods to handle errors, you can use these to stop your sequence from terminating when an error event occurs.

    A simple example that just ignores any errors would be to return nil whenever your getParamterDefinition observable emits an error:

    override func getParameters() -> Single<[ParameterModel?]> {
        return getParameterIds()
            .do(onSuccess: { [weak self] values in
                self?.numberOfParameterIds = Float(values?.count ?? 0)
            })
            .flatMap { ids in
                Single.zip(
                    ids!.compactMap {
                        self.getParameterDefinition(id: $0)?
                            .catchAndReturn(nil)
                    }
                )
            }
            .flatMap { parameters in
                Single.zip(
                    parameters.compactMap { parameter in
                        parameter.flatMap { self.getParameter(id: $0.id) }
                    }
                )
            }
    }