Hi,
i am running into a problem with the use of multiple switchMaps in ngrx Effects. The problem is not scoped only to ngrx though.
So i have an effect, that needs to call three different APIs after each other. It only makes sense in this order and there is no case where a single call to one of the APIs would make sense.
My initial code looked like this:
threeApiCalls = createEffect(() =>
this.actions.pipe(
ofType(MyActions.SomeAction),
switchMap(action => this.api.callOne(action.params)),
switchMap(resultOfCallOne => this.api.callTwo(resultOfCallOne)),
switchMap(resultOfCallTwo => this.api.callThree(resultOfCallTwo)),
map(result => MyActions.ReceiveResult({ result })),
catchError(error => of(MyActions.ReceiveResultError({ error })))
)
)
Then i ran into the problem, that an error in one of the api calls would complete the observable and thus the effect. For the people not familiar with ngrx effects: An Effect should not complete. If it completes, it is useless.
The solution to that is to catch errors on every of the calls:
threeApiCalls = createEffect(() =>
this.actions.pipe(
ofType(MyActions.SomeAction),
switchMap(action => this.api.callOne(action.params).pipe(catchError(err => of(err)))),
switchMap(resultOfCallOne => this.api.callTwo(resultOfCallOne).pipe(catchError(err => of(err)))),
switchMap(resultOfCallTwo => this.api.callThree(resultOfCallTwo).pipe(catchError(err => of(err)))),
map(result => MyActions.ReceiveResult({ result })),
catchError(error => of(MyActions.ReceiveResultError({ error })))
)
)
Now the observable does not complete on error. But i have the very ugly problem that i need to introduce type checks to every of the API calls after the first one because it may have received an error:
threeApiCalls = createEffect(() =>
this.actions.pipe(
ofType(MyActions.SomeAction),
switchMap(action =>
this.api.callOne(action.params).pipe(catchError(err => of(new MyErrorClass(err))))
),
switchMap(resultOfCallOne => {
if (resultOfCallOne instanceof MyErrorClass) {
return of(resultOfCallOne);
}
return this.api.callTwo(resultOfCallOne).pipe(catchError(err => of(new MyErrorClass(err))));
}),
switchMap(resultOfCallTwo => {
if (resultOfCallTwo instanceof MyErrorClass) {
return of(resultOfCallTwo);
}
this.api.callThree(resultOfCallTwo).pipe(catchError(err => of(new MyErrorClass(err))));
}),
map(result => {
if (result instanceof MyErrorClass) {
return MyActions.ReceiveResultError({ error: result });
}
return MyActions.ReceiveResult({ result });
},
catchError(error => of(MyActions.ReceiveResultError({ error }))))
)
);
This is a lot of overhead. :(
Now for my actual question/proposal:
Is it possible to propagate errors from switchMaps inner observable to its source observable (wihout completing), so that i only have to catch them in a single catchError and don't have to worry about the type checking in any of the subsequent switchMaps? And if no, is it possible to write an operator that has this behavior?
You should be able to use the second argument that is passed to the catchError selector. It's a reference to the source observable:
threeApiCalls = createEffect(() =>
this.actions.pipe(
ofType(MyActions.SomeAction),
switchMap(action => this.api.callOne(action.params)),
switchMap(resultOfCallOne => this.api.callTwo(resultOfCallOne)),
switchMap(resultOfCallTwo => this.api.callThree(resultOfCallTwo)),
map(result => MyActions.ReceiveResult({ result })),
catchError((error, source) => source.pipe(
startWith(MyActions.ReceiveResultError({ error }))
))
)
)
thanks a lot @cartant Your solution is solving my problem! As I use this approach in many places of our app I wrapped it into a custom operator to reduce the overhead even more.
export const catchSwitchMapError = (errorAction: (error: any) => any) => <T>(source: Observable<T>) =>
source.pipe(catchError((error, innerSource) => innerSource.pipe(startWith(errorAction(error)))))
final result:
threeApiCalls = createEffect(() =>
this.actions.pipe(
ofType(MyActions.SomeAction),
switchMap(action => this.api.callOne(action.params)),
switchMap(resultOfCallOne => this.api.callTwo(resultOfCallOne)),
switchMap(resultOfCallTwo => this.api.callThree(resultOfCallTwo)),
map(result => MyActions.ReceiveResult({ result })),
catchSwitchMapError(error => MyActions.ReceiveResultError({ error }))
)
);
Most helpful comment
You should be able to use the second argument that is passed to the
catchErrorselector. It's a reference to the source observable: