Angular 9/rxjs: How to handle an error thrown inside switchMap?
Asked Answered
S

2

5

I'm using Angular (9) powered Bootstrap (6.1.0) TypeAhead and defining its search function like so:

search = (text$: Observable<string>) => {
    return text$.pipe(
      debounceTime(200),
      distinctUntilChanged(),
      // switchMap allows returning an observable rather than maps array
      switchMap((searchText) => {
        if (!searchText || searchText.trim().length == 0) {
          // when the user erases the searchText
          this.dealerRepUserID = 0;
          this.dealerRepChanging.emit(this.dealerRepUserID);
          return EMPTY;
        }
        else if (this.dealerID == this.hostOrganizationID) {
          // get a list of host reps
          return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive);
        } else {
          // get a list of dealer reps
          return this.myService.getDealerReps(this.dealerID, searchText);
        }
      })
    );
  }

The function must return an Observable. How do I catch an error thrown inside the switchMap?

Schock answered 25/6, 2020 at 4:17 Comment(1)
It depends what error. You can use try-catch on the whole block or you can catch error notifications emitted by this.myService.get* calls with catchError() operator depending on what you want to do. Or you can also put catchError() after switchMap.Strategic
S
8

Have you try the catchError

import { catchError } from 'rxjs/operators';
return text$.pipe(
      debounceTime(200),
      distinctUntilChanged(),
      // switchMap allows returning an observable rather than maps array
      switchMap((searchText) => {
        if (!searchText || searchText.trim().length == 0) {
          // when the user erases the searchText
          this.dealerRepUserID = 0;
          this.dealerRepChanging.emit(this.dealerRepUserID);
          return EMPTY;
        }
        else if (this.dealerID == this.hostOrganizationID) {
          // get a list of host reps
          return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive).pipe(catchError(error => of());
        } else {
          // get a list of dealer reps
          return this.myService.getDealerReps(this.dealerID, searchText).pipe(catchError(error => of());
        }
      })
    );

Here is my app effect

public loadDataPerformance$: Observable<Action> = createEffect(() => {
    return this.actions$.pipe(
      ofType(RiskProfileActions.loadDataPerformance),
      withLatestFrom(
        this.store$.select(fromRoot.getAnalyticsFilterSelectedOptions),
        this.store$.pipe(select(fromFactoryPerformance.getFactoryId))
      ),
      switchMap(([{ recordDate }, filters, factoryId]) =>
        this.riskProfileApiService.getDataPerformanceData(filters, factoryId, recordDate).pipe(
          map((riskDataPerformanceData: PerformanceDataModel) =>
            RiskProfileActions.loadRiskScoreBreakdownPerformanceSuccess(riskDataPerformanceData)
          ),
          catchError(error => of(RiskProfileActions.loadRiskScoreBreakdownPerformanceFail(error)))
        )
      )
    );
  });

Squeegee answered 25/6, 2020 at 4:24 Comment(0)
C
14

The switchMap by itself won't throw any error, the thing that might do something unexpected are the returned observables this.myService.getHostRepsAutoComplete and this.myService.getDealerReps. A tricky moment with the catching errors is that whenever there is an error the observable which is throwing the error is being killed.

For example

observable$.pipe(
 switchMap(() => observable2$),
 catchError(() => doSomethingFunction())
).subscribe()

observable$ will be completed once there is an error, that will complete your search stream and you will get no more data after the error.

As Phat Tran Ky showed in his example the handling of errors should happen inside the new streams in the switchMap operator

observable$.pipe(
 switchMap(() => observable2$.pipe(catchError(() => doSomethingFunction())),
 )
).subscribe()

By doing so whenever there is an error thrown from inside it will kill the inner observable (observable2$) but won't kill the outer subscriptions on the outer observable observable$

A further enhancement that you can do in order to handle your errors in one spot might be to merge the inner observable in one, for example, something like

observable$.pipe(
 switchMap(() => {
   return merge(
   observable1$.pipe(filter(() => ${your if else condition for case 1})),
   observable2$.pipe(filter(() => ${your if else condition for case 2})),
   observable3$.pipe(filter(() => ${your if else condition for case 3})),
   ).pipe(catchError((error) => yourErrorHandlerFunction(error)))
  })),
 )
).subscribe()
Coracorabel answered 25/6, 2020 at 10:14 Comment(1)
I'm reluctant to nest pipes. An option is to use the retry operation from rxjsBasket
S
8

Have you try the catchError

import { catchError } from 'rxjs/operators';
return text$.pipe(
      debounceTime(200),
      distinctUntilChanged(),
      // switchMap allows returning an observable rather than maps array
      switchMap((searchText) => {
        if (!searchText || searchText.trim().length == 0) {
          // when the user erases the searchText
          this.dealerRepUserID = 0;
          this.dealerRepChanging.emit(this.dealerRepUserID);
          return EMPTY;
        }
        else if (this.dealerID == this.hostOrganizationID) {
          // get a list of host reps
          return this.myService.getHostRepsAutoComplete(searchText, this.includeInactive).pipe(catchError(error => of());
        } else {
          // get a list of dealer reps
          return this.myService.getDealerReps(this.dealerID, searchText).pipe(catchError(error => of());
        }
      })
    );

Here is my app effect

public loadDataPerformance$: Observable<Action> = createEffect(() => {
    return this.actions$.pipe(
      ofType(RiskProfileActions.loadDataPerformance),
      withLatestFrom(
        this.store$.select(fromRoot.getAnalyticsFilterSelectedOptions),
        this.store$.pipe(select(fromFactoryPerformance.getFactoryId))
      ),
      switchMap(([{ recordDate }, filters, factoryId]) =>
        this.riskProfileApiService.getDataPerformanceData(filters, factoryId, recordDate).pipe(
          map((riskDataPerformanceData: PerformanceDataModel) =>
            RiskProfileActions.loadRiskScoreBreakdownPerformanceSuccess(riskDataPerformanceData)
          ),
          catchError(error => of(RiskProfileActions.loadRiskScoreBreakdownPerformanceFail(error)))
        )
      )
    );
  });

Squeegee answered 25/6, 2020 at 4:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.