To unsubscribe, or not to unsubscribe, that is the question. — Subscriptions in RxJS

Jurek Wozniak
17 min readSep 28, 2021

--

Some say RxJS is all about Observables. While I can agree with that to some extent, I will be a bit more controversial and say that RxJS is all about Subscriptions. Let me justify this. An Observable on its own does not do anything. It just stores the logic how will the notifications be generated once it is executed. A Subscription is what executes the code placed inside of an Observable and puts it into action. So the Subscriptions are what actually makes things happen. With that comes a great responsibility — as some of the Observables can emit the values in an endless open-ended fashion, we need to make sure that the Subscriptions are closed at some point, so a proper cancellation, clean up and disposing of the resources can happen.

Photo by Dim Hou on Unsplash

The Subscription can end in a few ways. One of the ways is to call .unsubscribe() on the Subscription we want to close. The topic when and whether to unsubscribe can get quite confusing. In this article, I will try to demystify the topic of the Subscription.

What happens if I do not unsubscribe?

RxJS is all about Subscriptions. They execute the Observables. Leaving active unused Subscriptions, in the best case scenario, can lead to minor memory leaks. In worse scenarios and when your app runs for longer, you will experience memory and/or performance issues. In the worst case, your app will act strangely and might even crash.

We need to make sure that the Subscriptions end at some point, and they are not hanging in the memory endlessly. To write decent and reliable RxJS code it is essential to understand how Subscriptions work and when to unsubscribe.

You do not always have to unsubscribe. In fact it is best to write your code in a way the Subscriptions are mostly handled for you — we will get to this later.

In this article you will learn how the Subscriptions work to understand what is the Subscription’s lifecycle and how to find out whether you need to unsubscribe from the Observables you have in your code or not.

What is a Subscription?

Before we get into the details, we need to clarify what a Subscription actually is. Very often it is understood as something much more complicated than it actually is.

To cut the long story short, a Subscription is like a call to the function with the logic placed inside of the Observable to which we are subscribing.

To better understand what I mean by the above, let us see what the Observable actually is.

What an Observable is?

An Observable on its own does not do anything. It just describes the logic, how the values will be generated when this logic gets executed. In other words, an Observable is just an object with some rules and guarantees regarding its capabilites and interface. It is focused around a function containing the logic of the Observable. This function is passed to the new Observable constructor as an argument and describes the way the notifications will be emitted.

This Observable will emit two values (next notifications) ‘A’ and ‘B’ and then immediately complete.

Most often, instead of using the new Observable constructor — as seen above, you will use some helper functions creating Observables, such as of(), from(), timer(), interval() and many many others, however each of these functions will actually use the new Observable constructor inside and return the resulting Observable. What is important to know at this point is that each Observable has this function embedded inside, and this function describes the logic of the Observable — how the notifications will be generated.

Each time you call .subscribe(...) on the Observable, this internal function in the Observable is called. So, each Subscription is a separate execution of the Observable’s logic.

Try this out in this coding sandbox below.

In the above example you can see that there is a console.log inside of the function passed as the logic of the Observable.

An Observable which logs to console each time its logic gets executed.

Then each time we would make a new Subscription to this Observable by calling observable$.subscribe(), a separate execution of the Observable will happen. So if we would subscribe to this Observable three times, we would see this console.log three times as the Observable’s logic will be executed three times — independently for each Subscription. This is how RxJS is designed. Each time you subscribe to an Observable, the logic inside of the Observable gets executed. That is a very important thing to know to understand Subscriptions well — the Subscriptions are not something really complex and magical. In essence they are just a way to call the logic inside of the Observable.

What are the Subscriptions for?

To cut the long story short, the main thing is that Subscriptions enable us to cancel the execution of an Observable. This is not particulary useful in cases like the previously mentioned, where the Observable instantly emitted two values and then completed. Let us have a look at a few examples, where this might be useful:

  • DOM event emitters — a very popular source of Observables which can be created by using the fromEvent() function; imagine that we enter some view in our app and start listening to the ‘click’ event of some button, then get notified each time such event gets dispatched, and finally, when we are no longer interested in these events, we might want to stop receiving them,
  • WebSockets — we might want to start the connection and then if we are no longer interested in the data, we might want to stop receiving the values and end the connection,
  • interval generators — we might have an Observable endlessly generating values every few seconds, if we would not have the option to cancel such timer, we would endlessly get triggered with updates coming from this Observable,
  • …tons of others!

As you can see there are many cases in which we have ‘subscribable’ sources, to which we want to connect and disconnect. That is probably why the Observable’s execution is called a Subscription. As RxJS can do a lot of things it was difficult to find a better abstraction of what it does and how to name it.

Fun fact: Subscription was previously called Disposable, so it was even more confusing than now.

As a side note, if we would name things differently — subscribing is executing the logic inside of the Observable and unsubscribing is a way to cancel these executions. So when we call .unsubscribe() on one of our Subscriptions we are actually cancelling this particular execution of the Observable.

How can a Subscription end?

You create a new Subscription by calling the .subscribe(...) method on the Observable. This executes the Observable’s logic.

Then, as long as the Subscription works, the Observable can emit values by emitting any (0…N) number of the next notifications.

This Subscription will be present in the memory until it gets closed.

There are three ways in which a Subscription can end:

  • The Observable can emit a complete notification.
  • The Observable can emit an error notification.
  • Your code can call .unsubscribe() on the Subscription.
Keeping the reference to the Subscription object, so we can then call the unsubscribe() method on it.

So, when do I need to unsubscribe?

As you could see there are three ways in which the Subscription can end, or in other words, get closed. Two are determined by the Observable’s logic — emitting a complete or error notification. And one is in our hands — unsubscribing.

Most often you will want your Subscriptions to happen when you open some view or some component would show up on screen. Then, your Subscriptions would react to various events, perform API calls, etc. Finally, when you close that view or hide the component you would want to make sure that these Subscriptions are closed and do not cause further reactions and/or hang in the memory.

In short, every Subscription should end somehow, when it is no longer needed — either by receiving a complete or error notification or by unsubscribing. If you make sure that this happens for all of your Subscriptions, you are good and your app should not have memory leaks and issues related to leaving open Subscriptions.

Note: An exception from the above rule is that if you want to have some Subscriptions running all the time while your app runs. You can leave them going all the time without unsubscribing, as that is what you want in these cases.

Complete or Error notification

The complete notification is used to signal that the Observable has done its job and will not emit any more values. The error notification is used to inform that something failed and the Observable is not able to emit further values.

In both cases, these notifications mean that the Observable will not emit any further values (next notifications) after emiting either of them.

An Observable emitting a complete or error notification

After the Observable emits either of these notification, the Subscription will end immediately, so the Observable will not emit any further values (next notifications) and we do not need to unsubscribe.

Unsubscribing

There are some Observables which never complete or error. In such case, if we want to stop the Subscription and, in other words, the execution of our Observable, we can unsubscribe. Calling the .unsubscribe() method on the Subscription will also end the Subscription, the same way as the complete and error notifications emitted by the Observable did.

An example of an open-ended Observable

In the above case, we have an Observable endlessly emitting values until the Subscription gets closed. After we call .unsubscribe() on each active Subscription we are good and everything is cleaned up properly.

Mixed case

There might be some cases in which the Observable emits the complete or error notifications at some point, but it is not guaranteed that it will always happen.

The above code will complete after we click on the button a single time, however if we never do this the Subscription will keep on being active waiting for this single click to happen. So it is safe to assume that we should unsubscribe, when we are no longer interested in waiting for this ‘click’ event. For example, if the component or view with that button would get hidden.

Generally, if you are not sure whether the Observable will emit or has emitted a complete or error notification, you can always call .unsubscribe() — just to be safe. This will make sure that the Subscription gets closed if it was not and perform proper clean-up. If it already was previously closed, nothing extra will happen.

What happens after the Subscription ends?

To have a full picture of the Subscription’s lifecycle, I will show you what happens when the Subscription ends.

By ‘ending the Subscription’ I mean all ways in which the Subscription can get closed:

  • the Observable’s logic emitting the complete or error notification
  • our code calling .unsubscribe() on our Subscription

After any of the above happens, whichever happens first, the Finalizer/Teardown logic is called.

If you have more time, see the video presenting the whole Subscription’s Lifecycle:

If you have less time, here it is summarized:

Lifecycle of a Subscription — from subscribing, through the execution of the Observable and emitting notifications, until the Subscription ends and the Finalizer/Teardown logic gets run

The Finalizer/Teardown logic is provided by the function inside of the Observable and is used to perform clean-up and dispose actions by the Observable’s logic and to release any used resources, abort pending connections and processes.

An Observable providing a Finalizer/Teardown logic — the above is similar to the Observable created using the fromEvent(…) function

Subscribing to the above Observable will add another handler to the button and unsubscribing will remove the handler.

Summarizing, after the Subscription ends, no matter why — by complete or error notifications or unsubscribing — you are good to go. Remember to make sure that all Subscriptions to your Observables are ended. You might have many Subscriptions made to one Observable and each one of them is a separate execution of the Observable, so you need to make sure that all of them are closed at some point. Of course, an exception is if you want certain Subscriptions to be active for the whole lifetime of your application.

Examples: How to find out if I need to unsubscribe?

This is it as far as the theory behind the Subscriptions goes. Let us now see a few scenarios and try to investigate how do they work as far as the need for unsubscribing is concerned.

It is easy to judge whether we need to unsubscribe in a simple case where we know the Observable’s logic well. What about more complex scenarios? How to determine whether some Observable completes or not?

Generally to have the answer you need to investigate the whole logic of your Observable step-by-step.

  • What is the main source of the Observable? Does it ever complete? When?
  • How are the notifications generated by the source?
  • How each of the operators affect the Observable? Does it cause it to complete? Under what circumstances?

Answering the above questions should guide you towards the answer whether the Observable reliably and always completes or whether you should make sure that your code ends the Subscription explicitly.

Simple scenario — of()

In the above case our source is the Observable created by using the of() function. Each Subscription to such Observable will immediately receive a set of ‘next’ notifications with the provided values (‘Alice’, ‘Ben’ and ‘Charlie’) and then complete. As a way to test this, you can try the following code:

This will provide you with the answer what actually happens. After running such code, your console will read:

Value: Alice
Value: Ben
Value: Charlie
Completed

This means that each time you subscribe to this Observable it will instantly emit all values and complete. And as we already know — after a Subscription receives the complete notification it ends, so there is no need to unsubscribe.

Another simple scenario — fromEvent()

Let us start with the source. It is an Observable created with the fromEvent function. This means, that this Observable never completes or errors. Because of that this Subscription never ends on its own and in this particular case we need to unsubscribe if we want to stop handling the above ‘click’ event.

A way to automatically complete a Subscription is to add a take(X) operator to the pipeline.

This code will react to the first click and then automatically complete, so theoretically you do not need to unsubscribe. That is true if you are sure that this ‘click’ will always happen. If this single ‘click’ would not happen, this Subscription would hang in the memory and be active waiting for that single click to happen. In such case it is best to unsubscribe if, for example, this code is specific to some component and this component is about to get hidden.

More complex scenario — multiple operators

As you could see above, the operators can influence whether the whole Observable created by applying operators completes or not, even if the main source does not complete. However, in the below case:

We have multiple operators and the whole Observable created by using the fromEvent() function and then applying three operators map, filter and tap still never completes. This means that it is important to look at the operators from the perspective of how they handle the Subscriptions and whether they emit the complete or error notifications at some point. The above operators are transparent in this matter — they do not emit the complete notification at any point, contrary to the take(X) operator which we have used previously. The operators here can error if we would provide some faulty code inside of the functions which we have passed to them, but that is an exception handling thing, rather than something we can rely on to always happen.

Even more complex scenario — mergeMap()

This example is a possible memory leak trap! Watch out for such cases in your apps. We should definitely unsubscribe from such Subscription, that is due to the fact that the source Observable is created with fromEvent which does not complete on its own. Also, we do not have any operators in the pipeline which could change this.

Now, why is this example such a danger to the memory leaks and this pattern might cause performance issues if the calculations inside would get more complex?

There is a mergeMap operator used which creates a new inner Subscription for each value/‘input’ event incoming from the source — fromEvent. This inner Subscription is made to the inner Observable created using the interval function, which is also a never-ending Observable endlessly emitting values in intervals. And what is worse, as these inner Subscriptions do not complete, they will be working as long as the main Subscription is active, due to the way mergeMap operator works. So after many ‘input’ events coming from the fromEvent Observable there will be a ton of inner Subscriptions going on.

If you would forget to unsubscribe from the whole outer Subscription, the memory and performance of the app might suffer after some time.

In the above case it is possible that, instead of mergeMap, another flattening operator should be used, such as switchMap — depending on the desired behavior. The switchMap operator would end the previous inner Subscription and start a new one for the new value, so there will be at most one active inner Subscription.

Regardless of the above — you should make sure that you unsubscribe from this Observable when you no longer need it as it will not end on its own.

Even more complex scenario

In the above scenario we have a quite complex Observable. Let us now investigate what happens there to determine whether we need to unsubscribe or whether this Observable will complete on its own.

  1. The main source of the emissions is an Observable created by the interval(3000) function. This means that once we subscribe to it, it will emit a value every 3 seconds and never complete.
  2. Then, each notification will reach the map operator, which just maps the value to something else. So this operator does not introduce anything which could complete this Observable.
  3. At this stage of the pipeline we have the exhaustMap operator, which will create new inner Subscription based on the value coming from above. So right now we have to take this inner Subscription into consideration — we need to analyze it separately.
    Note: If you are not yet familiar with how the flattening operators such as switchMap, mergeMap, concatMap and exhaustMap work, it is worth to learn them as they are one of the most useful and at the same time tricky operators to use. I will not explain them in detail here as it is a fairly complex topic that could make a separate article.
  4. As menitoned above, the exhaustMap operator will make a Subscription to the provided inner Observable making a new HTTP request to the server by using an Observable created by the ajax function. This Observable always completes or errors after the request succeeds or fails.
    For more about the ajax() function see: https://medium.com/@jaywoz/ground-control-to-major-tom-http-calls-in-rxjs-1d47ba964b6c
  5. Next, we have two more operators: timeout and catchError. Notice, that these operators are nested. In other words, they are directly applied to the Observable created with the ajax() function.
  6. This means that each notification coming from the Observable created with the ajax function will first reach the timeout and catchError operators. Multiple things can happen at this stage. If the HTTP call fails or the a timeout happens, the timeout operator will emit an error which then would be caught by the catchError operator which will map the outcome to an EMPTY Observable which just immediately completes.
  7. Then, the outcome from the combination ajaxtimeoutcatchError will reach and be processed by the exhaustMap operator’s logic. The exhaustMap operator passes the values (next notifications) and errors further and does not pass the complete notification coming from the inner Observable further. So, even though the inner Observable might complete, the outer/main Subscription will keep on working further.
  8. Finally, we reach the end of the pipeline of operators, reaching the handlers inside of the subscribe call at the end.

Gathering all the above points we have a never-ending interval Observable as the source. So we need to unsubscribe from it at some point as it will never end on its own. Another thing to note here is that we have the exhaustMap flattening operator which makes additional inner Subscriptions to the Observable made from connecting the ajax, timeout and catchError operators. This inner Observable completes, however due to the way the exhaustMap flattening operator works this complete notification is not passed further down the operator’s chain.

Summarizing, you should also make sure that you unsubscribe in this case.

Multiple Subscriptions

If you make multiple Subscriptions to your Observable, remember that each Subscription is a seperate execution of the Observable, so you need to make sure that all Subscriptions end at some point.

The above example is trivial, however sometimes you might have many Subscriptions to the same Observable scattered across your app. From RxJS 7.3.0, you can use the tap operator to track them when new Subscriptions are made to your Observable and when they end.

The above will allow you to debug your code and track when new Subscriptions are made to your Observable and whether and when are they closed.

Other cases

Is your case not covered here? Feel free to post a comment with your case, so we can see and investigate your case!

Also, if you are not sure what happens in your case it is worth to put some console.log spies in your code. Please check my article on how you can see what happens in your pipeline of operators: https://medium.com/@jaywoz/information-is-king-tap-how-to-console-log-in-rxjs-7fc09db0ad5a

Try not to need to unsubscribe — Other ways to unsubscribe

There are multiple ways which help you to handle the topic of Subscriptions.

take(X)

If you are interested in receiving just one value and then ending the Subscription, you can use the take operator.

The above will make the Subscription complete after a single emission.

Note: If your Observable emits just one value and completes, you do not need to do the above, as the result will be exactly the same.

takeUntil

You can use an Observable, which can be used to trigger the completion of the Observable.

The below Observable will react to the ‘mousedown’ event, then track the ‘mousemove’ events until the user triggers the ‘mouseup’ event.

Also, you can have multiple Subscriptions and then use a single Subject to complete all of them.

When you initialize your component you can create multiple Subscriptions to various Observables and by using a common Subject and the takeUntil operator, you can make all of these Subscriptions complete, by making this Subject emit a single notification.

Angular: `| async` pipe

This is a great tool which does all the subscribing and unsubscribing for you. When some component or even certain part of your component, which provides a value using the | async pipe, gets shown on the screen, this pipe will automatically subscribe to your Observable and once this part gets hidden/destroyed, this Subscription will get closed.

The TypeScript part of the component

This is a pseudocode of a component providing two Observables.

  • isLoggedIn$ is an Observable using an NgRx-style state,
  • items$ fetches the items from an endpoint.

Let us now have a look at the template part of this component.

The template part of the component

In the first line, the async pipe will subscribe to isLoggedIn$. Whenever a truthy value gets emitted by this Observable, the list will be shown on the screen. When this happens, another Subscription will be made — this time in line 3. The async pipe will subscribe to the items$ Observable, which will make a connection to the server and present the items in the template, once the result comes.

Then, if the isLoggedIn$ would emit a fasly value, the list would be hidden. The async pipe will handle the unsubscribing for us if needed.

Also, if the whole component gets destroyed, the Subscription to isLoggedIn$ (line 1) will also be closed.

Angular: Auto-unsubscribing libraries

You can use libraries which make it easier to handle the Subscriptions. For example, they can automatically unsubscribe when the component gets destroyed.

One of my favorites is the @ngneat/until-destroy npm package.
See: https://github.com/ngneat/until-destroy

Final words

As you can see the topic of Subscriptions can be confusing, so understanding what the Subscriptions and Observables actually are is crucial to know when when should we unsubscribe and whether we need to do this at all.

Make sure you understand the source Observable which emits the values and all how all operators you are using work — mostly from the perspective of when and whether they complete.

If you use Angular, take advantage of the | async pipe if it works for you in your particular scenario.

If you have enjoyed this article and want more — please give a clap and follow me: Medium, Twitter, LinkedIn. Cheers!

New to RxJS?

If you are new to RxJS or would like to polish your knowledge, I invite you to have a look at my RxJS and Observables: Introduction course.

--

--