Search code examples
angularionic3rxjsangular-httpclient

Angular HttpClient Get Service Returning Too Early


I'm trying to convert my old Ionic Angular Http code to the new Httpclient format, but the Get service is returning control to the calling function too early, so it doesn't get the returned data.

I've tried using async/await, but it makes no difference to the control flow.

I'm new to observables, so I'm sure it's something I'm not doing correctly, but I can't figure out what.

These are the functions from my code, with the new format of the getAsJson function, using the subscribe functionality of Httpclient. I only want to return the data from the http call, so I haven't included "observe: 'response'" in the options parameter.

loadLoyaltyDetails calls getLoyaltyDetails, which does a few other things than shown here and then calls getAsJson.

Functions:

loadLoyaltyDetails(): Promise<void> {
  return this.loyalty.getLoyaltyDetails().then(data => {
    console.log("in loadLoyaltyDetails, data =", data);
  })
  .catch(this.handleServiceLoadingError.bind(this))
}

getLoyaltyDetails(): Promise<LoyaltyDetails> {
  return this.callApi.getAsJson("/loyalty/url");
}

getAsJson(path: string): Promise<any> {
  let opts = this.getRequestOptions();
  console.log("in getAsJson, path = ", path);

  return this.http.get<HttpResponse<HttpEventType.Response>>(`${environment.API_URL}${path}`, opts)
    .subscribe(
      (res) => {
        console.log("in getAsJson, res = ", res);
        return res;
      },
      (err) => {
        console.log("in getAsJson, err = ", err);
        this.checkResponseForUnauthorized(err);
      }
    )
}

Console Log Messages

in getAsJson, path = /loyalty/url

in loadLoyaltyDetails, data = 
Object { closed: false, _parent: null, _parents: null, _subscriptions: (1) […], syncErrorValue: null, syncErrorThrown: false, syncErrorThrowable: false, isStopped: false, destination: {…} }

Using current token

in getAsJson, path = /other/url

in getAsJson, res = {…}
    endDate: "2019-01-08"
    numberOfShiftsRequired: 18
    numberOfShiftsWorked: 0
    startDate: "2019-01-08"

in getAsJson, res = Array []

As the log messages show, loadLoyaltyDetails calls getAsJson first, but gets a bunch of gobbledegook back straight away. Then getAsJson is called by another function, receives back the data for the first call, then the second call.

I was expecting the 'in loadLoyaltyDetails, data = ' line to appear after the first set of data is returned.

This is what I can't figure out i.e. how can I ensure control is not returned to loadLoyaltyDetails before the data has been returned?


Solution

  • The subscribe function returns a Subscribtion object immediately and won't pause the execution of your code until the subscribed observable actually emits a value. Subscribtion objects aren't used to get the data from the Observable but only to unsubscribe from the Observable you previously subscribed to (Note that you don't have to unsubscribe from Observables returned by the HttpClient as they complete and therefore unsubscribe automatically).

    By calling return this.http.get(..).subscribe(..) you return this (to you useless) Subscribtion object all the way to your loadLoyaltyDetails() function, where you log it as the data object.

    Instead you should return Observables up until the point where you actually need the data from the Observable (i suppose this is loadLoyaltyDetails() for you). This is where you subscribe and in the subscribe function you do the things you need to do with the objects emitted by you're Observable (in your case the http response body). Typically you would set some component variable that is displayed in your html template to a value emitted by an Observable. You could even defer the subscribtion to your template with the AsyncPipe and don't subscribe manually at all.

    If you don't need to handle the full HttpResponse but only want to get the JSON body and handle errors you could do something like:

    localLoyaltyDetails: LoyaltyDetails;
    
    // Note that this function returns nothing especially no Subscribtion object
    loadLoyaltyDetails(): void {
      // supposing this is where you need your LoyaltyDetails you subscribe here
      this.loyalty.getLoyaltyDetails().subscribe(loyaltyDetails => {
        // handle your loyaltyDetails here
        console.log("in loadLoyaltyDetails, data =", loyaltyDetails);
        this.localLoyaltyDetails = loyaltyDetails;
      });
    }
    
    getLoyaltyDetails(): Observable<LoyaltyDetails> {
      // We call getAsJson and specify the type we want to return, in this case 
      // LoyaltyDetails. The http response body we get from the server at the given url, 
      // in this case "/loyalty/url", has to match the specified type (LoyaltyDetails).
      return this.callApi.getAsJson<LoyaltyDetails>("/loyalty/url");
    }
    
    // Don't subscribe in this function and instead return Observables up until the 
    // point where you actually need the data from the Observable.
    // T is the type variable of the JSON object that the http get request should return.
    getAsJson<T>(path: string): Observable<T> {
      let opts = this.getRequestOptions(); 
      console.log("in getAsJson, path = ", path);
    
      return this.http.get<T>(`${environment.API_URL}${path}`, opts)
        .pipe(
          // you could peek into the data stream here for logging purposes 
          // but don't confuse this with subscribing
          tap(response => console.log("in getAsJson, res = ", response)),
          // handle http errors here as this is your service that uses the HttpClient
          catchError(this.handleError) 
        );
    }
    
    // copied from the Angular documentation
    private handleError(error: HttpErrorResponse) {
      if (error.error instanceof ErrorEvent) {
        // A client-side or network error occurred. Handle it accordingly.
        console.error('An error occurred:', error.error.message);
      } else {
        // The backend returned an unsuccessful response code.
        // The response body may contain clues as to what went wrong,
        console.error(
          `Backend returned code ${error.status}, ` +
          `body was: ${error.error}`);
      }
      // return an observable with a user-facing error message
      return throwError(
        'Something bad happened; please try again later.');
    };
    

    You can read more about the HttpClient and the handleError function in the Angular HttpClient Docs. You could also write a handleError function that returns a default value on errors like the one in the Angular Tutorial (Http Error Handling).


    Edit regarding your comment:

    Generate an Observable from your Promise with the defer function (the generation of the Observable, and thus the execution of the Promise, is deferred until a subscriber actually subscribes to the Observable).

    import { defer } from 'rxjs';
    
    // defer takes a Promise factory function and only calls it when a subscriber subscribes 
    // to the Observable. We then use mergeMap to map the authToken we get from  
    // getLoggedInToken to the Observable we get from getAsJson.
    getLoyaltyDetails(): Observable<LoyaltyDetails> {
      return defer(this.login.getLoggedInToken)
        .pipe(
          mergeMap(authToken =>
            this.callApi.getAsJson<LoyaltyDetails>(authToken, "/loyalty/details/NMC")
          )
        );
    }
    

    Note that loadLoyaltyDetails returns nothing i.e. void.

    private details: LoyaltyDetails;
    
    loadLoyaltyDetails(): void {
      // supposing this is where you need your LoyaltyDetails you subscribe here
      this.loyalty.getLoyaltyDetails().subscribe(loyaltyDetails => {
        console.log("in loadLoyaltyDetails, data =", loyaltyDetails);
    
        // set a local variable to the received LoyaltyDetails
        this.details = loyaltyDetails;
      });
    }
    

    As your loadLoyaltyDetails returns nothing you just call the function at the time you need it to be executed.

    this.loader.wraps<void>(
      this.loadShifts().then(() => this.loadLoyaltyDetails())
    );