To unsubscribe, or not to unsubscribe, that is the question. — Subscriptions in RxJS
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.
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.
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.
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.
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.
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.
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
orerror
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:
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.
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.
- 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. - 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. - 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 asswitchMap
,mergeMap
,concatMap
andexhaustMap
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. - 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 theajax
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 - Next, we have two more operators:
timeout
andcatchError
. Notice, that these operators are nested. In other words, they are directly applied to the Observable created with theajax()
function. - This means that each notification coming from the Observable created with the
ajax
function will first reach thetimeout
andcatchError
operators. Multiple things can happen at this stage. If the HTTP call fails or the a timeout happens, thetimeout
operator will emit an error which then would be caught by thecatchError
operator which will map the outcome to anEMPTY
Observable which just immediately completes. - Then, the outcome from the combination
ajax
—timeout
—catchError
will reach and be processed by theexhaustMap
operator’s logic. TheexhaustMap
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. - 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.
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.
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.
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.