JS Promises #3: Garbage collection and memory leaks
Tags: javascript, promise, gc, programming
Published: 2023-01-19 21:07:04

Promises in JavaScript are a way to handle asynchronous operations. One of the trickiest part of async operations is that we may accidentally have a memory leak and therefore it's extremely important to understand when the promises is GCed and what prevents a promise from being garbage collected. 

**TL&DRs**
1. [This codesandbox](https://codesandbox.io/s/promises-article-first-example-8jfyh?file=/src/index.js) has all the examples for promises and then chains. Open sandbox and reload it. You can run `Promise example` first and `then example` after.

2. In the end of the article you will find key findings from the article.

----

{% spoiler Some notes about tools we will use %} 
This article is for advanced users, it researches how GC (garbage collector) works with promises. If you want to get base information about promises, check earlier articles in the series.

We will use `FinalizationRegistry` to find when the element is GCed. Check [this article](https://dev.to/xnimorz/javascript-memory-management-101-strong-and-weak-refs-finalizationregistry-1281) if you're not familiar with `FinalizationRegistry`.
To speed up GC we will create tons of mock object and remove strong refs to them. 

Please, do not run the experiments in dev tools console. Dev Tools keeps all the objects alive and therefore you will have false-negative results.

FinalizationRegistry prints in the console the line and the time spend.

It is important to keep in mind that if a promise is eligible for GC, it does not mean that it will be immediately collected. The GC process in JavaScript is not deterministic and occurs at the discretion of the JavaScript runtime.

{% endspoiler %}

Let's draft possible scenarios:

### Explicitly keep the reference to the Promise:

```js
const promise = new Promise((resolve, reject) => {...});
```

In this case we have a strong reference to the Promise and we won't GC it as long as `const promise` exists. 

📝 As long as you keep explicit reference to your promise, it won't be GCed

### Lose the references to promise, resolve and reject functions:

```js
let promiseWithoutResolve = new Promise((resolve) => {
  setTimeout(() => {
    console.log("Timeout for the promise that keeps no refs");
  }, 100000);
});

finalizationRegistry.register(promiseWithoutResolve, " which keeps no references"); 
promiseWithoutResolve = null;
```
For that test we keep neither strong ref to the `promise`, nor `resolve` function, however, the promise function has quite a long timeout operation. 

📝 When you lose all refs to the `resolve`, `reject` and instance itself the object is marked to GC and as soon as GC starts it will be collected. (Note: JS GC has several generations and therefore if your promise is in the 3rd generation, it might not be collected). 

### Cache `resolve` and/or `reject` method without strong ref to the promise instance itself

In real codebases you can find something similar to:
```js
let resolve;
let promise = new Promise((_resolve) => { 
  resolve = _resolve;
});
// Let's remove the reference to promise
promise = null;
```

This code performs 2 things:
1) Removes the strong reference to promise
2) Caches `resolve` function outside callback in the promise constructor. 

We don't have a direct access to the promise anymore but it's unclear if this promise will be queued for GC or it will stay as long as we keep the link to the `resolve`. 

To test this behaviour we can draft an experiment:
```js
let promiseWithResolve = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 100000);
});

finalizationRegistry.register(promiseWithResolve , " which keeps resolve function"); 
promiseWithResolve = null;
```
For such an experiment we have an output:
```
Promise  which keeps resolve function. Time taken: 155765. 
```

📝 The promise will be kept in memory as long as we have the reference to `resolve` or `reject` callbacks. 
📝 If we have a promise which lives for a long period of time it may get to the older generation, which means it will take even more time between losing all the refs to the promise and getting it GCed.

### `.then` chaining

Day-to-day use case:
```js
new Promise(() => {}).then(() => {/* Some async code */}) 
```

Experiment to test:

```js
let promiseWithThen = new Promise(() => {});
let then = promiseWithThen.then(() => {
  console.log("then reached");
});

finalizationRegistry.register(promiseWithThen, " with `then` chain");
finalizationRegistry.register(then, " then callback");

promiseWithThen = then = null;
```

The output:
```
Promise  with `then` chain. Time taken: 191. 
Promise  then callback. Time taken: 732. 
```
The original `promiseWithThen` will never be resolved but it's chained by following `then` operation, which may keep the reference to the original promise. Luckily, `then` doesn't prevent promise from being GCed. 

📝 `.then` doesn't prevent promise from GC. Only explicit references to promise itself, `resolve` and `reject` matter.

## What would happen if we add `.then` to these experiments?

As we found, `.then`. doesn't prevent promises from garbage collection. Which means, it shouldn't have any effect. 

To prove this we can draft the experiment which is under `Then example` in this sandbox: https://codesandbox.io/s/promises-article-first-example-8jfyh?file=/src/index.js

When you run the experiment you will see that `then` really has no effect and it doesn't change any behaviour.

## Why several .then chains keep promise alive? 

Sometimes in the code we have .then chains:

```js
Promise.resolve()
  .then(asyncCode1)
  .then(asyncCode2)
  ...
  .then(asyncCodeN);
```
Even though we don't keep reference to the promise, it isn't scheduled to be GC before the chain completes. 
The thing is: `Promise.resolve()` returns the resolved promise, and the first `.then` plans a microtask to run, since the previous promise is resolved. So, we will have a planned scope to execute. 

`.then` returns a promise which is chained by the following async operation (`asyncCode2`, ...). And the value for the async operation is the return value of the previous `.then` block (in our case it's the fn `asyncCode1`). 

📝 if your `.then` chain gets "control" (planned for execution or even started execution), the function scope has the reference to resolve or reject the promise which is returned by `.then` and therefore this promise won't be GCed.

# To sum up:

Short list of key findings:

📝 References to: the `promise instance` itself, `resolve`, `reject` keep the promise from being garbage collected.

📝 `.then` doesn't prevent promise from GC.

📝 if your `.then` chain gets "control" (planned for execution or even started execution), the function scope has the reference to resolve or reject the promise which is returned by `.then` and therefore this promise won't be GCed.

📝 If we have a promise which lives for a long period of time it may get to the older generation, which means it will take even more time between losing all the refs to the promise and getting it GCed.