47

I'm building an architecture on latest Angular 6 and coming from AngularJS there's something I can't make peace about: the basic processing of an HTTP request.

So, for the sake of the question, let's say I want an observable. Because it seems to be the future of Angular.

I went from something very elegant, in AngularJS :

   service.getAll()
    .then(onSuccess) // I process the data
    .catch(onError) // I do whatever needed to notify anyone about the issue
    .finally(onFinally); // I stop the loading spinner and other stuff

Now in Angular 6/RxJS 6, I don't understand why everything is so complicated and look not right.

I could find two ways of doing the same thing than above:

  1. The full pipe

    this.service.getAll()
        .pipe(
            map((data) => this.onSuccess(data)),
            catchError(error => of(this.handleError(error))),
            finalize(() => this.stopLoading())
        )
        .subscribe();
    

Since we have to use pipe for the finalize, I might as well use pipe for everything, I think it's better practice to have everything in the same order. But now we have to throw something, called "of" (not very easy to understand) and I don't like that.

  1. The half pipe So I try another idea, with only the pipe I need (finalize) and I keep the subscribe callbacks.

    this.service.getAll()
    .pipe(
        finalize(() => this.stopLoading())
    )
    .subscribe(
        (data) => this.onSuccess(data),
        (error) => this.handleError(error)
    );
    

But, well. Isn't it a little bit backward? We still have callbacks without actual names, and we finalize before reading the processing and the error. Weird.

So there is something I definitely don't understand. And I can't find anything related to this basic question online. You either have someone who wants "success and finally", or "success and error" but noone wants the 3 of them. Maybe I'm too old and I don't understand the new best practice about that (if so, please educate me!).

My need is simple:
1. I want to process the data I get from a service
2. I want to get the error in order to display to the user
3. I want to stop the loading spinner I just started before the call, or make another call once the first one is complete success or error (I really want a finally)

How do you handle your basic HTTP call with observable?

(I don't want any .toPromise, please, I want to understand how to do with the new stuff)

BlackBeard
  • 10,246
  • 7
  • 52
  • 62
Simon Peyou
  • 693
  • 1
  • 6
  • 13

3 Answers3

47

I think there's one key misunderstanding:

You either have someone who wants "success and finally", or "success and error" but none wants the 3 of them.

This isn't entirely true. Each Observable can send zero or more next notifications and one error or complete notification but never both. For example when making a successful HTTP call you'll have one next and one complete notification. On error HTTP request you'll have only one error notification and that's all. See http://reactivex.io/documentation/contract.html

This means you'll never have an Observable emitting both error and complete.

And then there's the finalize operator. This operator is invoked when disposing the chain (which includes plain unsubscribing as well). In other words it's called after both error and complete notifications.

So the second example you have is correct. I understand it looks weird that you include finalize before subscribing but in fact each emissions from the source Observable goes first from top to bottom where it reaches subscribers and there if its error or complete notification it triggers dispose handlers bottom up (in opposite order) and at this point finalize is called. See https://github.com/ReactiveX/rxjs/blob/master/src/internal/Subscriber.ts#L150-L152

In your example using finalize is the same as adding the dispose handler yourself into a Subscription objects.

const subscription = this.service.getAll()
  .subscribe(
    (data) => this.onSuccess(data),
    (error) => this.handleError(error)
  );

subscription.add(() => this.stopLoading());
martin
  • 93,354
  • 25
  • 191
  • 226
  • 3
    This is very close to what I had in mind. Thank you for the explanation. I just have the word "add" instead of "finally" but since it's in the right reading order, I think this is ok. `this.service.getAll().subscribe(success,error).add(finally);` – Simon Peyou Jun 28 '18 at 14:46
  • 1
    @SimonPeyou I'm on the same spot as you, jumping from Promises to Observables is so mindblowing rough due this strange naming conventions. On HTTP requests I prefer to have interceptors to catch errors and let the promises/observables fail with a finally to clean things up. – Diosney Jan 02 '19 at 05:20
  • Great explaination, although it took me some time to find the description of add(), [Adds a tear down to be called during the unsubscribe() of this Subscription](http://reactivex.io/rxjs/class/es6/Subscription.js~Subscription.html#instance-method-add) – Kim Nyholm Dec 23 '19 at 17:44
9

The subscribe method of Observable accepts 3 optional functions as parameters

  • the first one to process the data which come with the event raised by the Observable
  • the second one to process any error if it occurs
  • the third one to do something on completion of the Observable

So, if I understand correctly, what you want can be achieved with code that looks like this

this.service.getAll()
.subscribe(
    data => this.onSuccess(data),
    error => this.handleError(error),
    () => this.onComplete()
);

Consider that using Observables for http calls can give benefits when you want to retry (see retry operator) on in case you have race conditions (via the use of switchMap operator). I think these are the main reasons why Angular team has chosen this approach for the http client.

Generally speaking I think it is worth starting to know how Observables work and some of the most important operators (in addition to the ones above, I think first about mergeMap, filter, reduce - but then there are many others) is important because they can significantly simplify many tasks in asynchronous non blocking environments, such as the Browser (or Node for instance).

Picci
  • 16,775
  • 13
  • 70
  • 113
  • 5
    Sadly, and correct me if I'm wrong, the "complete" argument in the `.subscribe` signature is fired when everything went well. It's not fired in case of an error. I understand the benefits of Observable, I'm not advocating against them. I just want to be sure how to do this basic (and maybe why it is not the basic good practice anymore). – Simon Peyou Jun 28 '18 at 14:08
  • Yes, you are right, either `complete` or `error` or nothing, for Observables which never complete. I do not understand the sad part though. – Picci Jun 28 '18 at 14:13
  • Same comment as above, and I'm starting to think I miss something obvious about it: in my example where do I call the stopLoading then? – Simon Peyou Jun 28 '18 at 14:16
  • 1
    @Picci I think what he wants is finally not completed which is called only after success. What he want is like a closure after either success or failure. Check this https://stackoverflow.com/a/44771000/667767 to see the different between completed and finally aka finalize – maxisam Mar 03 '19 at 05:37
3

I think the right way is to use Observable functions: next, err, complete.
Here is an example of how you can trigger the complete function.
Let say we have BehaviorSubject object:

let arr = new BehaviorSubject<any>([1,2]);

Now lets assume we want to subscribe to it, and if we get the value "finish" we want to complete.

let arrSubscription = arr.asObservable().subscribe(
  data => {
      console.log(data)
      if(data === 'finish') {
        arr.complete()
      }
  },
  err => {
      console.log(err)
  },
  () => {
      console.log("Complete function triggered.")
  }
);
arr.next([3,4])
arr.next('finish')
arr.next([5,6])

The console log is:

[1,2]
[3,4]
finish
Complete function triggered.

Since we triggered the complete function, the last .next to our BehaviorSubject wont fired, since err and complete function are terminators of the subscription.
This is just an example how u can triggered the complete function, from here you can do what ever you want.

dAxx_
  • 2,210
  • 1
  • 18
  • 23
  • 2
    I'm sorry I don't see how this is working with Angular HTTP observable. If I'm in error, I need to call complete as well then. I might as well call my postCompletion functions there. – Simon Peyou Jun 28 '18 at 14:12
  • No, error and complete are terminators of the subscription while data is infinite. If you have error, you should handle the error, and its not the same behavior as handle a complete. – dAxx_ Jun 28 '18 at 14:14
  • In my example where do I call the stopLoading then? – Simon Peyou Jun 28 '18 at 14:16
  • 1
    if stopLoading is a function that act the same both in error and in complete, than you have to call on both... in error you might wanna do something differently , lets say display a message to user plus error icon, and in complete you want just stop the spinner. its all depends on your needs. – dAxx_ Jun 28 '18 at 14:23
  • So there is no point in finalize, then. In this case, I mean. – Simon Peyou Jun 28 '18 at 14:26
  • From my knowledge, Finalize is a function that call when error or complete, so its contains both callbacks, but if you wanna handle errors, i dont think its the right choice. its better to split the actions between error and complete. But its your call. Hope you got it right, good luck – dAxx_ Jun 28 '18 at 14:30