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.