1
votes

After one of my reducers throws an exception the store.dispatch method stops working. For example:

 function Reducer(currentState, action){
   switch(action.type){
      case 'BLOW':
        throw "BANG!"
      case 'TEST':
        console.log('OK');
        return currentState;
  }

I dispatched 'TEST' twice and it was all good, i.e. 'OK' was printed. Then I dispatched 'BLOW', which threw an exception. After that, nothing happens when I dispatch 'TEST' again. The action never reaches the reducer.

enter image description here

I saw some answer related to handling errors inside Effects, but I am not using Effects at all. Is there a way to make the store go back to a consistent state after a reducer throws an exception?

I am using Angular 5.2.9 and ngrx 4.1.1

2

2 Answers

1
votes

I was able to make it work using a meta-reducer and a service.

You use an injection token to add a meta-reducer which has an Angular service as a dependency. The meta-reducers wraps the reducer around a try / catch block and lets the service knows what was the result of dispatching the action.

@NgModule({
  providers: [
    {
      provide: META_REDUCERS,
      deps: [StoreExceptionCatcher],
      useFactory: getMetaReducers
    }
  ]
})
export class AppModule {}

export function getMetaReducers(storeExceptionCatcher: StoreExceptionCatcher): MetaReducer<any>[] {
    /** 
     * Guarantees store will still be in a consistent state if reducers throw an exception.
     * Notifies StoreExceptionCatcher of the result of dispatching a given action.
     */
    function exceptionCatcher(reducer: ActionReducer<any>): ActionReducer<any> {
        return function(state, action) {
            try{
                state = reducer(state, action);
                storeExceptionCatcher.sendResult(action);
            }catch(err){
                storeExceptionCatcher.sendResult(action, err);
            }
            /* simply returns the old state if action throws an exception */
            return state;
        };
    }

    return [exceptionCatcher];
}

This is how the service looks like:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';

@Injectable()
export class StoreExceptionCatcher{
    private resultObs: Subject<DispatchResult> = new Subject<DispatchResult>();

    /** 
     * Returns a promise that will resolve or fail after 'action' is dispatched to the store.
     * The object passed to this function should be the same as the one passed to store.dispatch.
     * This function should be called before 'store.dispatch(action)'.
     */
    waitForResult(action: any): Promise<void>{
        return this.resultObs.filter(result => result.action === action).take(1).toPromise().then(res => {
            if(res.error){
                throw res.error
            }
        });
    }

    /** Should only be used by meta-reducer */
    sendResult(action: any, error?: any): void{
        this.resultObs.next({action, error});
    }
}

export interface DispatchResult{
    action: any,
    error?: any;
}

Now, to dispatch an action to the store you can do this:

 private dispatch(action: any): Promise<void>{
        const promise = this.storeExceptionCatcher.waitForResult(action);
        this.store.dispatch(action);
        return promise;
    }

This will make sure the store continues to work if a reducer throws an exception and it also provides a way to get a promise that resolves or fails depending on the result of dispatching a given action.

0
votes

This can also occur (in my experience) when subscribers to selects from the store throw exceptions in their subscription handlers, so even if your pure reducers never throw exceptions, you can end up in this state.

In our case, upgrading to Angular 6 / NGRX 6 seemed to address this issue. I would suggest upgrading to v6 of both then testing again to see if it still occurs.