12

I am trying to send 2 HTTP requests one by one; if the first one is succeeds, send the second one, if not display the corresponding error message regarding to the first request.

I am planning to use something like that, but not sure if it is the best option for this scenario:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: 'app/app.component.html'
})
export class AppComponent {
  loadedCharacter: {};
  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http.get('/api/people/1').subscribe(character => {
      this.http.get(character.homeworld).subscribe(homeworld => {
        character.homeworld = homeworld;
        this.loadedCharacter = character;
      });
    });
  }
}

I have different requests e.g. PUT and CREATE also using this approach. I know there are other ways e.g. forkjoin, mergemap, but if this one solves my problem seems to be more readable. Any idea?

Prag Dopravy
  • 201
  • 1
  • 3
  • 12
  • [This may give you some inspiration](https://medium.com/better-programming/rxjs-patterns-emerging-from-stackoverflow-asynchronous-api-calls-as-streams-in-the-real-world-ef636c9af19a) – Picci Nov 13 '20 at 14:03
  • @Picci Seems to be very useful, I will definitely read. But for now, I need an urgent solution. Any suggestion pls? – Prag Dopravy Nov 13 '20 at 14:15
  • [Here](https://stackoverflow.com/a/63685990/6513921) is a short answer that I wrote on handling nested observables. – ruth Nov 13 '20 at 14:49

4 Answers4

12

First of all, your code works and that's great - you can leave it as is and everything will be fine.

On the other hand, there is a way for multiple improvements that will help you and your colleagues in future:

  1. try to move http-related logic to the service instead of calling http in the components - this will help you to split the code into view-related logic and the business/fetching/transformation-related one.
  2. try to avoid nested subscribes - not only you ignore the mighty power of Observables but also tie the code to a certain flow without an ability to reuse these lines somewhere in the application. Returning the Observable might help you with "sharing" the results of the request or transforming it in some way.
  3. flatMap/mergeMap, concatMap and switchMap work in a different way, providing you an ability to control the behaviour the way you want. Though, for http.get() they work almost similar, it's a good idea to start learning those combining operators as soon as possible.
  4. think about how you'll handle the errors in this case - what will happen if your first call will result an error? Observables have a powerful mechanism of dealing with them, while .subscribe allows you to handle an error only in one way.

An example using the switchMap:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: 'app/app.component.html'
})
export class AppComponent {
  loadedCharacter: {};
  constructor(private http: HttpClient) {}

  ngOnInit() {
    const character$ = this.http.get('/api/people/1').pipe(
      tap(character => this.characterWithoutHomeworld = character), // setting some "in-between" variable
      switchMap(character => {
        return this.http.get(character.homeworld).pipe(
            map(homeworld => {
                    return {
                        ...character,
                        homeworld: homeworld
                    }
                }
            )
        )
      }),
      catchError(errorForFirstOrSecondCall => {
        console.error('An error occurred: ', errorForFirstOrSecondCall);
        // if you want to handle this error and return some empty data use:
        // return of({});
        
        // otherwise: 
        throw new Error('Error: ' + errorForFirstOrSecondCall.message);
      })
);

    // you can either store this variable as `this.character$` or immediately subscribe to it like:
    character$.subscribe(loadedCharacter => {
        this.loadedCharacter = loadedCharacter;
    }, errorForFirstOrSecondCall => {
       console.error('An error occurred: ', errorForFirstOrSecondCall);
    })
  }
}


Yevhenii Dovhaniuk
  • 1,073
  • 5
  • 11
  • Thanks a lot for your detailed explanations. Actually that's why I asked this question even if it would work. So, in this case could you post the code above by usimng one of your suggested ways e.g. `flatMap/mergeMap` etc.? I would prefer a most readable way so that I can manipulate the results easily. – Prag Dopravy Nov 13 '20 at 14:25
  • You get max upvotes, but it would be better to suggest some usage. Thanks in advance. – Prag Dopravy Nov 13 '20 at 14:56
  • @PragDopravy I've added an example with `switchMap` – Yevhenii Dovhaniuk Nov 13 '20 at 17:38
  • Sorry, but I am not sure if I explained the scenario very well. Asssume that I have 2 different API call for updating data and; **1.** In each call I want to get the response of that call and catch the error related to that call. **2.** If the first call is failed, I want to break and does not execute the second call after getting error or checking response data. So, I think it seems to be better to use `switchMap` as you gave. So, could you please update your answer according to the scenario I explained here? – Prag Dopravy Nov 14 '20 at 22:26
  • The scenario above with fail on either first call error or second call error. The errors can be caught either in `.subscribe`'s callback or at the very end of the `character$`'s `.pipe` (`catchError`). The second call won't be performed if the first one results an error. If you want to set some data from the first call simply move it to a separate variable or use `.tap(data => this.data = data)` (but from your example you almost completely ignore the response of the first call) – Yevhenii Dovhaniuk Nov 16 '20 at 08:34
  • Could you updet the answer regarding to the scenario above? If it is possible I wouls like to use `switchMap` in that example. – Prag Dopravy Nov 16 '20 at 11:05
  • Done - `switchMap`, `in-between` variable storing and `catchError` are present in the example – Yevhenii Dovhaniuk Nov 16 '20 at 12:43
  • Thnanks for detailed explanation. **1.** As far as I see, one catch block is would be enough for multiple calls or `switchMap` blocks. Is that true? **2.** On the other hand, what if I need to make more calls? In that case should I add multiple `switchMap` block just below the one one your code? **3.** What should I use to finish the stream? There are `return NEVER`, `return EMPTY`, etc, but I have no idea about which one I should do? – Prag Dopravy Nov 17 '20 at 07:21
  • Yes, `catchError` will catch all the errors if it's placed in the end. More calls - more `switchMaps`, that's also true (simply copy the first one). You don't need to finish the stream - it's auto-finished by the Angular's `http` – Yevhenii Dovhaniuk Nov 17 '20 at 09:36
  • Thanks, but if I want to finish the stream at any point e.g. aplying a logic or the call result is not suitable for the condition, I think I should finalize. Otherwise the result will pass to the next stream (next `switchMap`). Is that true? **the second question** is when I do not use `return NEVER` or `return `serv,ce.method(..)` it gives error. So, do I have to use one of them in the `switchMap` ? – Prag Dopravy Nov 17 '20 at 10:47
  • Try to specify your scenarios more clearly - clear steps and what do you wan't to achieve. The chain of `pipe-able` operators can solve any task related to the stream while some `if`s inside `switchMap` can control the flow of your logic. From the example above - there is no correct way of `finalising` the stream, you should think of splitting them into more variables and combine them the way you want. Also, your `switchMap` can return either `this.http.get` or `of(null)` if you don't want to perform some http calls depending on the condition. – Yevhenii Dovhaniuk Nov 17 '20 at 14:57
3

2 nested subscriptions are never a way to go. I recommend this approach:

this.http.get('/api/people/1').pipe(
  switchMap(character => this.http.get(character.homeworld).pipe(
    map(homeworld => ({ ...character, homeworld })),
  )),
).subscribe(character => this.loadedCharacter = character);

Edit: For your university

this.http.get('/api/people/1').pipe(
  switchMap(character => this.http.get(character.university).pipe(
    map(university => ({ ...character, university})),
  )),
).subscribe(character => this.loadedCharacter = character);

Or even chain university and homeworld requests

this.http.get('/api/people/1').pipe(
  switchMap(character => this.http.get(character.homeworld).pipe(
    map(homeworld => ({ ...character, homeworld })),
    // catchError(err => of({ ...character, homeworld: dummyHomeworld })),
  )),
  switchMap(character => this.http.get(character.university).pipe(
    map(university => ({ ...character, university})),
  )),
).subscribe(character => this.loadedCharacter = character);
MoxxiManagarm
  • 8,735
  • 3
  • 14
  • 43
  • Could you please and an update to your answer for this scenario? First you get a person as above and then update hirs or her university name by callling another request? – Prag Dopravy Nov 13 '20 at 14:14
  • Thnaks a lot. Does it continue to execute the second call even if there is an error at the first one? On the other hand, how can I handle errors for each of these calls? – Prag Dopravy Nov 13 '20 at 14:35
  • 1
    By the way voted up, thanks for your helps. I would be appreciated if you explain a little bit how it works also. Regards. – Prag Dopravy Nov 13 '20 at 14:35
  • With this chain the second call wouldn't be executed if the first fails. But you could add a `catchError` after the `map` operator, I again updated my answer, I only added an example as a comment. With the `catchError` the outer stream would continue. – MoxxiManagarm Nov 13 '20 at 14:40
  • I am so sorry, but I need some clarification. Actually you send get requests and add the returned `homeworld` to the `character` array. Is that true? But actually I want to send firts request add and then by getting its id, send another request to add another record (lets assume that add a person and then automatically add default education info as primary school). So, if you would not mind could you pls add 3 rd scenario for that? The second would also be useful for someone else and it is good idea to keep it. I think your approach is very useful and thats why I want to use it :) regards. – Prag Dopravy Nov 13 '20 at 15:31
  • If you have time and add an update I would be appreciated and will use that approach. – Prag Dopravy Nov 13 '20 at 16:52
3

You can try a solution using switchmap and forkJoin for easier chaining and error handling. this will help keep the code clean in case the chain keeps growing into a deep nest.

    this.http
      .get("/api/people/1'")
      .pipe(
        catchError((err) => {
          // handle error
        }),
        switchMap((character) => {
          return forkJoin({
            character: of(character),
            homeworld: this.http.get(character.homeworld)
          });
        })
      )
      .subscribe(({ character, homeworld }) => {
        character.homeworld = homeworld;
        this.loadedCharacter = character;
      });

EDIT: Scenario 2

this.http
      .get("/api/people/1")
      .pipe(
        catchError((err) => {
          console.log("e1", err);
        }),
        switchMap((character) => {
          return forkJoin({
            character: of(character),
            homeworld: this.http.get(character.homeworld).pipe(
              catchError((err) => {
                console.log("e2", err);
              })
            )
          });
        })
      )
      .subscribe(({ character, homeworld }) => {
        character.homeworld = homeworld;
        this.loadedCharacter = character;
      });

You can chain a catch error or add a separate function for error handling without it invoking the next API call. but I would recommend abstracting the backend logic to an angular service and using this method. which would help retain an easy to read structure.

Avindu Hewa
  • 1,608
  • 1
  • 15
  • 23
  • 1
    Seems to be good instead of using nested ones. Thanks, voted up. – Prag Dopravy Nov 13 '20 at 14:27
  • 1
    Could you also please add another usage with this approach as following? At first request updating a record and then according to the returned value (eg. status code)send another requesr for adding another record and display message? – Prag Dopravy Nov 13 '20 at 14:31
  • so you only want to send the second request if the status code is lets say 200? – Avindu Hewa Nov 13 '20 at 14:33
  • 1
    Not actually, I mean that at first call add a record and then send another request for updating another record. But while this, I want to handle errors for each request and if there is an error in the first request I want to break and just display erros message without executing the second one. – Prag Dopravy Nov 13 '20 at 14:37
0

You can check if the first request was successful or not by checking the status code:

  ngOnInit() {
    this.http.get('/api/people/1').subscribe((character: HttpResponse<any>) => {
      // here you should look for the correct status code to check, in this example it's 200
      if (character.status === 200) {
        this.http.get(character.homeworld).subscribe(homeworld => {
          character.homeworld = homeworld;
          this.loadedCharacter = character;
        });
      } else {
        // character is gonna contain the error
        console.log(character)
      }
    });
  }
Elmehdi
  • 1,390
  • 2
  • 11
  • 25