Search code examples
angularreactive-programmingangular-reactive-forms

Handle http error using switchMap and return empty Observable


Just want to ask how to properly handle this scenario. I have a form which built using reactive form. This form loads the details of the selected user that I have selected from the list (users) to edit the details.

Displaying the data in the form is working as expected. Below is my code implementation.

In my service, I have this code to pull a single record from an API.

SERVICE

    private selectedUserIdSubject = new BehaviorSubject<number | null>(null);
    selectedUserIdAction$ = this.selectedUserIdSubject.asObservable();

    user$ = this.selectedUserIdAction$
        .pipe(
            switchMap(id => this.httpClient.get<IUserInfo>(`${environment.apiBaseUrl}/users/{id}`)
            ),
        );

NOTE: When calling the API with ID is not exist, it returns an 503 Service Unavailable error.

COMPONENT

    ngOnInit() {
        this.route.params.subscribe(params => {
          this.selectedUserId = +params['id'];

          this.userService.onSelectedUserId(this.selectedUserId);
        })

        this.initUserForm();
    }

    selectedUser$ = this.userService.user$
        .pipe(
            tap(data => {

              if (data !== null) {
                this.loadUserDetails(data);
              }
            })
    );

HTML

<div class="form-container mat-elevation-z8" *ngIf="selectedUser$ | async as user">
    <form class="form-container" [formGroup]="userForm">
        <mat-form-field>
            <input matInput placeholder="Firstname*" formControlName="FirstName">
        </mat-form-field>
        <mat-form-field>
            <input matInput placeholder="Lastname*" formControlName="LastName">
        </mat-form-field>
        <mat-form-field>
            <input matInput placeholder="Address*" formControlName="Address">
        </mat-form-field>
    </form>
</div>

Now, I decided to use the same form for creating a new record. My approach would be, when I clicked the "NEW BUTTON", I should be redirected to form (Edit) with a parameter of -1 to indicate that I'll be creating a new record and not loading the details with -1 ID.

My problem now is that, since I'm using a reactive programming approach, every time my component loads, the selectedUser$ observable always trigger and my service trigger an http request to the API and trying to find a user detail with -1 ID.

I'm thinking in my service, when the ID is -1, I will return an empty Observable with IUserInfo type. But seems not working and the error still persist as stated in the above error.

    user$ = this.selectedUserIdAction$
        .pipe(
            switchMap(id => {
                if (id !== -1) {
                    this.httpClient.get<IUserInfo>(`${environment.apiBaseUrl}/users/{id}`
                } else {
                    return new Observable<IUserInfo>();
                }
            })
            ),
        );

What I want is that, when I have a -1 parameter, It should load and initialize the form for creating a new record.

TIA!


Solution

  • First option is to complete an observable sequence, i.e.

    .pipe(
        switchMap(_ => this.httpClient.get<IUserInfo>(`/users/{id}`))
        catchError(userInfo => {
            // Do something with error, like log, etc
            return empty()
        })
    )
    

    in this case, if previous observable (httpClient) throws an error, it will be completed, ie subscription section will not be reached... Ideal case in your case

    Second option is to return predictable results, for example if endpoint returns an array, you can return an observable of empty array

    .pipe(
        switchMap(_ => this.httpClient.get<IUserInfo>(`/users/{id}`))
        catchError(userInfo => {
            // Do something with error, like log, etc
            return of({})
        })
    )
    

    Third option is to property handle error in subscriber, but it will not be your case as long as you use async pipe

    .pipe(
        switchMap(_ => this.httpClient.get<IUserInfo>(`/users/{id}`))
    )
    .subscribe(res => {}, err => {
        // return predictable results here
    })
    

    Although I'd recommend to achieve this with ErrorHandler Interceptor and additional checks. Error handler interceptor will catch all server error and handle error accordingly.. In your case you also can prepend api origin via interceptor, so you don't have to add it manually to each service

    @Injectable({
        providedIn: 'root'
    })
    export class ApiInterceptor implements HttpInterceptor {
        public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
            // Appending environment.origin only if url is not starts with protocol
            if (!req.url.match(new RegExp('^https?://'))) {
                req = req.clone({
                    url: environment.origin + req.url
                });
            }
    
            return next.handle(req).pipe(
                catchError(error => {
                    if (error instanceof HttpErrorResponse) {
                        // Do something with an error here
                    }
                    return throwError(error);
                })
            );
        }
    }
    
    .pipe(
        switchMap(_ => this.httpClient.get<UserInfo>(`/users/{id}`)),
        map(userInfo => !userInfo || typeof userInfo !== 'object' ? {}  : userInfo),
        catchError(err => empty())
    )
    

    Also, get through TypeScript Coding Guidelines . IUserInfo is not complied with it...