Ernesto G
Ernesto G

Reputation: 545

retryWhen only works as expected in the first error

I have this setup: A user inputs an employee code, then I make an http call to an API that returns some extended data. If the code is incorrect, the API responds with a "Not Found" Error. I had the problem that the Observable just ended the subscription if the user types an incorrect code (as expected according to the specification), so I am using the retryWhen operator to handle the error and resubscribe, expecting the user to input a correct code.

The problem is that the logic inside retryWhen is only being executed for the first time an error happen. If the user fails two times, or even if the user fails once, then inputs a right code and then another time inputs a bad one, the code inside retryWhen is not reached.

export class EmployeeService {
  constructor(private http: HttpClient) { }

  getEmployee(code: string): Observable<Employee> {
    const baseUrl = config.employeesUrl
    return this.http.get<Employee>(`${baseUrl}/${code}`)
  }
}
import { Component, OnInit } from "@angular/core";
import { Cart } from "src/app/interfaces/cart.interface";
import { fromEvent, timer } from "rxjs";
import { debounce, switchMap, retryWhen } from "rxjs/operators";
import { EmployeeService } from "../../services/employee.service";
import { AlertController } from "@ionic/angular";

@Component({
  selector: "app-set-employee",
  templateUrl: "./set-employee.page.html",
  styleUrls: ["./set-employee.page.scss"]
})
export class SetEmployeePage implements OnInit {
  constructor(
    private employeeService: EmployeeService,
    private alert: AlertController
  ) {}

  cart: Cart = {
    employee: { code: null, name: null },
    transactions: null
  };

  async presentAlert() {
    const alert = await this.alert.create({
      header: "Error",
      subHeader: "",
      message: "Some Alert",
      buttons: ["OK"]
    });

    await alert.present();
  }

  ngOnInit() {
    const input = document.getElementById("codigo");
    const $input = fromEvent(<HTMLInputElement>input, "keyup");
    $input
      .pipe(
        debounce(() => timer(1000)),
        switchMap(event =>
          this.employeeService.getEmployee(event.target["value"])
        ),
        retryWhen(error => {
          console.log("retrying...");
          this.cart.employee.name = null;
          this.presentAlert();
          return error;
        })
      )
      .subscribe(
        data => {
          this.cart.employee = data;
        },
        error => console.log(error)
      );
  }
}

Upvotes: 1

Views: 612

Answers (2)

frido
frido

Reputation: 14099

The code inside retryWhen is only executed once on the first error. retryWhen(error$ => onError$) will then resubscribe to the source Observable when the Observable returned inside retryWhen (onError$) emits. The error$ Observable retryWhen receives as input emits whenever the source teminates with an error.

This means you have to add your error handling logic to the observable stream onError$ you return.

retryWhen(error$ => error$.pipe(
  tap(() => {
    console.log("retrying...");
    this.cart.employee.name = null;
    this.presentAlert();
  })
))

I don't think it's good code design to use retryWhen in this scenario.

Instead handle the error with catchError inside getEmployee(code: string). Error handling should be done in the Service not the Component. I wrote an answer about it here: Why handle errors with catchError and not in the subscribe error callback in Angular.

Infact I would move the whole alert on error logic away from the Component into a Service.

Service

constructor(private http: HttpClient, private alert: AlertController) { }

getEmployee(code: string): Observable<Employee> {
  const baseUrl = config.employeesUrl
  return this.http.get<Employee>(`${baseUrl}/${code}`).pipe(
    catchError(this.handleError('getEmployee', getAlertData(), {}))
  )
}

private getAlertData() {
  return {
    header: "Error",
    subHeader: "",
    message: "Some Alert",
    buttons: ["OK"]
  };
}

private handleError<T> (operation = 'operation', alertData?: any, result?: T) {
  return (error: any): Observable<T> => {
    console.log(`${operation} failed: ${error.message}`);

    // Let the app keep running by returning an empty result.
    return alertData 
      ? from(this.alert.create(alertData)).pipe(
        switchMap(alert => from(alert.present())),
        switchMap(_ => of(result as T))
      )
      : of(result as T);
  };
}

Component

private onDestroy$ = new Subject();

ngOnInit() {
  fromEvent(<HTMLInputElement>input, "keyup").pipe(
    debounce(() => timer(1000)),
    switchMap(event => this.employeeService.getEmployee(event.target["value"])),
    takeUntil(this.onDestroy$)
  ).subscribe(data => this.cart.employee = data);
}

ngOnDestroy() {
  this.onDestroy$.next();
  this.onDestroy$.complete();
}

Upvotes: 1

Hsuan Lee
Hsuan Lee

Reputation: 2330

You can use catchError operator.


  ngOnInit() {
    const input = document.getElementById("codigo");
    const $input = fromEvent(<HTMLInputElement>input, "keyup");
    $input
      .pipe(
        debounce(() => timer(1000)),
        switchMap(event =>
          return this.employeeService.getEmployee(event.target["value"])
                 .pipe(catchError(e => {
                   return of(null)
                  })
        ))
      )
      .subscribe(
        data => {
          if (data === null) {
            this.presentAlert();
          } else {
            this.cart.employee = data;
          }

        },
        error => console.log(error)
      );
  }

https://stackblitz.com/edit/angular-iqnvxx?file=src/app/app.component.ts

Upvotes: 1

Related Questions