Juri Strumpflohner

RSS

RxJS finalize operator to execute logic on Observable termination

Author profile pic
Juri Strumpflohner
Published

In this article we’re going to have a look at the RxJS finalize operator. To have a practical use case, let’s take a look at disabling/enabling a form submit button during an HTTP request.

Translations of this article: Chinese

TL;DR: Here’s the corresponding Egghead lesson

Disabling/enabling a button during an Angular HTTP request

Let’s take a look at an RxJS Observable subscription:

this.someService.fetchDataFromApi().subscribe(
  (result) => {
    // success
  },
  (err) => {
    // some error happened
  },
);

Assume this call is triggered by a button click on our form. Many people still double-click on those buttons and we definitely want to prevent 2 calls being sent to our backend API. There are different ways to avoid that of course, but for the purpose of this example, let’s go the route of disabling the button once it has been clicked, and re-enable it when the http call terminates.

this.isLoading = true;
this.someService.fetchDataFromApi().subscribe(
  (result) => {
    // success
    this.isLoading = false;
  },
  (err) => {
    // some error happened
    this.isLoading = false;
  },
);

Whenever isLoading is set, we disable our button on the form. Now as in the example before, the isLoading = false instruction is duplicated, because we want to re-enable the button in both, success and error cases.

Option 1: Using the tap operator?

One option could be the tap operator. For instance:

this.isLoading = true;
this.someService
  .fetchDataFromApi()
  .pipe(
    tap((_) => {
      this.isLoading = false;
    }),
  )
  .subscribe(
    (result) => {
      // success
    },
    (err) => {
      // some error happened
    },
  );

Using tap like this however, will only execute in case of a success and not when the observale throws an exception & terminates (such as in a failed Http call in Angular).

The operator however takes a config object that allows us to hook onto the next, error and complete event state of an Observable, very much like we can do in the subscribe.

this.someService
  .fetchDataFromApi()
  .pipe(
    tap({
      next: (x) => {
        console.log('tap success', x);
        this.isLoading = false;
      },
      error: (err) => {
        console.log('tap error', err);
        this.isLoading = false;
      },
      complete: () => console.log('tap complete'),
    }),
  )
  .subscribe(
    (x) => {
      console.log('Got result', x);
    },
    (err) => {
      console.error('Got error', err);
    },
  );

Here’s an example of using tap like that:

Option 2: Using the finalize operator!

Another option is to use the finalize operator. It’s like in the try-catch-finally programming construct which is present in most C based programming languages. Hence, we can modify our example from before to the following:

this.isLoading = true;
this.someService
  .fetchDataFromApi()
  .pipe(
    finalize(() => {
      this.isLoading = false;
    }),
  )
  .subscribe(
    (result) => {
      // success
    },
    (err) => {
      // some error happened
    },
  );

How does finalize work? It basically adds a callback to the teardown of the Observable, via subscription.add(fn). This guarantees it will be called on error, complete, and unsubscription.

Conclusion

Note, the finalize operator is executed whenever our Observable terminates. This is important! For Angular HTTP this works perfectly, because the Observable returned by the Angular HTTP service “completes” once the request is done. That might not be the case if you have a custom Observable.

Check out my corresponding video explaining the finalize operator or play directly with this Stackblitz code sample.

Happy coding!

Thx Ben Lesh for suggesting updates on the article