How to properly bubble up state or global error handler

Question regarding bubbling up action for a deep nested state, for example:

  • Handling user authenticated with state structured like this, app > landing> login > 2FA >
    A naive approach in app reducer will be :
case let .landing(.login(.twoFactor(.twoFactorResponse(.success(response))))),
         let .landing(.login(.loginResponse(.success(response)))) where !response.twoFactorRequired:
...
  • Globally handle 401 (unauthorized) response from remote server.
    A naive approach will be handling this in every reducer or combine it to bubble it up to app state.

Is there an elegant solution for such scenarios? One way i can think of is using long living effects with notification center and combine it with root/app reducer.

2 Likes

This is what we are trying solve here

Expecting some feedback from author and still we are not sure why Reducer can't return error.

As @grinder81 mentions above we've gone through this exercise for the same reason. We need to handle 401 but also we're showing a toast on errors too. So far, we're able to do this using our own FailableReducer type which returns a failable Effect<Action, Error>. We then implemented (copied) the combine, pullback, etc. functions to make everything work. So all the real reducers are FailableReducer.

At the root of the tree, we have a higher-order reducer which does the catch and performs the error handling. This higher-order reducer is the normal non-failable Reducer which can be passed into the Store:

extension Reducer {
    static func errorHandling(
        _ reducer: FailableReducer<AppState, AppAction, AppEnvironment>
    ) -> Reducer<AppState, AppAction, AppEnvironment> {
        Reducer { state, action, environment in
            reducer(&state, action, environment)
                .catch { error -> Effect<AppAction, Never> in
                    if case APIError.unauthorized = error {
                        return Effect(value: .handleUnauthorized)
                    }
                    return Effect(value: .showError(error))
                }
                .eraseToEffect()
        }
    }
}

One thing we've been wondering about is if it can make sense for Reducer to return failable Effect<Action, Error> instead of the non-failing Effect<Action, Never>. We tried on this branch and it seems to work out ok so far, but we're looking for feedback on it. We believe there might be a reason for this - whether it's technical or practical though we're not sure. Hoping it can be possible though as it seems it would make this a bit easier for centralized error handling.

2 Likes

By the way, the other approach we dabbled with was have the reducers return Effect<Result<Action, Error>, Never>. It didn't seem straightforward though so we gave up on it rather early.

@sundeepgupta @grinder81 Thank you, will keep watching for update on the branch.