Misc Promises Info

Nesting

Quite possibley the most useful feature of promises is nesting. Nesting refers to the fact that when a thenFunction (after another Promise) returns a Promise itself, the Promise chain waits until that returned Promise is furfilled. Let us examine the following code.

// Nesting Snippet #1
new Promise(function apple(accept) { accept("foo string example"); })
    .then(function bannana(foo){ // foo = "foo string example"
        // The variable returnPromiseVar is not neccecary. It is only
        // for the purposes of demonstration. It would work equally
        // well just to do return new Promise(/*...*/);
        var returnPromiseVar = new Promise(function coconut(accept){
            accept( "bar string example" );
        });
        return returnPromise;
    })
    .then(function date(bar){ // bar = "bar string example"
        /*...*/
    });

The way Promise chaining works is that the value of bar passed to the function date will be the return value of function bannana unless the function bannana  returns a Promise (which in this case it is a Promise). For purely demonstrational purposes, the Promise returned by bannana is assigned to the variable returnPromiseVar. Now, since Bannana returns the promise returnPromiseVar, the browser waits until returnPromiseVar is accepted or rejected before executing date with the resolved value of the Promise returnPromiseVar. An easy way to think of this is imagining the browser secretly doing returnPromiseVar.then(function date(){/*...*/}) behind the scenes. All three of the below samples do the exact same thing as  "Nesting Snippet #1."

new Promise(function apple(accept) { accept("foo string example"); })
    .then(function bannana(foo){ // foo = "foo string example"
        var returnPromiseVar = new Promise(function coconut(accept){
            accept( "bar string example" );
        }).then(function date(bar){ // bar = "bar string example"
            /*...*/
        });
        return returnPromise;
    });
new Promise(function apple(accept) { accept("foo string example"); })
    .then(function bannana(foo){ // foo = "foo string example"
        return new Promise(function coconut(accept){
            accept( "bar string example" );
        }).then(function date(bar){ // bar = "bar string example"
            /*...*/
        });
    });
new Promise(function apple(accept) { accept("foo string example"); })
    .then(function bannana(foo){ // foo = "foo string example"
        return new Promise(function coconut(accept){
            accept( 
                (function date(bar){ // bar = "bar string example"
                    /*...*/
                })("bar string example")
            );
        });
    });

If you are still lost, then no need to worry. To better visualize nesting, try running the below code in the console.

new Promise(
    accept=>{
        console.log("Checkpoint #1 @ ", (performance.now()/1000).toFixed(1)+"s");
        setTimeout(accept, 100, "uno");
    }
).then(
    first_result=>{
        console.log("1st: ",first_result, " @ ", (performance.now()/1000).toFixed(1)+"s");
        return new Promise(
            accept=>{
                console.log("Checkpoint #2 @ ", (performance.now()/1000).toFixed(1)+"s");
                setTimeout(accept, 100, "dos");
            }
        ).then(
            second_result=>{
                console.log("2nd: ", second_result, " @ ", (performance.now()/1000).toFixed(1)+"s");
                return "tres";
            }
        )
    }
).then(
    third_result=>console.log("3rd: ", third_result, " @ ", (performance.now()/1000).toFixed(1)+"s")
)

Memory Management

However, there is one major problem with Promises: memory. Memory can become a serious issue if you do not know what you are doing because of the ease in creating long-life objects. Examine the below example of the Fetch API.

fetch("https://www.example.com/index.html")
    .then(/*firstFunc*/ response => response.text());
    .then(/*secondFunc*/ textContent => { console.log('"Length: ' + textContent.length + '"'); });

Or, if you are unfamiliar with arrow functions:

fetch("https://www.example.com/index.html")
  .then(function firstFunc(response){ return response.text(); });
  .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); });

 

The above code style is pretty standard, so it must be pretty efficient, right? Think again. Observe the below persuado-visualization steps of the memory timeline. The number in curly-brackets at the start of each step represents the number of persuado-objects in memory before the step has taken place, and the number in curly brackets at the end of each step represents the number of persuado objects after the step has taken place. Please note that this is only a persuado-exemplification of specific parts of the memory. In reality, there is ALOT more going on behind the scenes.

  1. {0} On line 1, a Promise object is allocated and returned by fetch on line 1. We will call this promise FetchPromise. {1}
  2. {1} On line 2, function firstFunc is allocated for use in FetchPromise.then method. {2}
  3. {2} On line 2, FetchPromise.then is called with the allocated function firstFunc, returning a Promise we will call ThenPromise1st. {3}
  4. {3} Because there are no longer any references to FetchPromise (e.g. variables assigned to it or methods of it being called), the FetchPromise (the object itself, not the then-functions attatched to it) is garbadge collected and freed from memory. {2}
  5. {2} On line 3, function secondFunc is allocated for use in ThenPromise1st.then method. {3}
  6. {3} On line 3, ThenPromise1st.then is called with the allocated function secondFunc, returning a Promise we will call ThenPromise2nd. However, because ThenPromise2nd is never used, it is immediately garbadge collected. {3}
  7. {3} Because there are no longer any references to ThenPromise1st, it is garbadge collected.  {2}
  8. {2} Waiting for the headers to be recieved...... and still waiting........... then wait some more................................................................................................................... {2}
  9. {2} Now the headers are recieved and FetchPromise is accepted, executing, on line 2, firstFunc and allowing the instance of firstFunc to be garbadge collected because it will never be runned again. {1}
  10. {1} On line 3, while executing firstFunc, a Promise object is allocated and returned by response.text(). We will call this promise HTTPtextPromise because it resolves to the text content of the page at the requested URL. {2}
  11. {2} On line 3,  HTTPtextPromise.then is internally used up by the browser as this is a case of nesting promises. Then, the  HTTPtextPromise instance is no longer referenced, and is garbadge collected. {1}
  12. {1} Waiting for the body of the HTTP request to be downloaded...... and still waiting........... then wait some more............................................................... {1}
  13. {1} Now that the body of the HTTP request is downloaded, HTTPtextPromise resolves to the downloaded data, triggering ThenPromise2nd to be accepted, executing, on line 3, secondFunc and allowing the instance of secondFunc to be garbadge collected because it will never be runned again. {0}

The key to efficeint memory management is minimizing the memory taken up during times of wait so as to allow other programs running on the computer to have the memory they need. For instance, if your javascript code has many promises, each holding massive chunks of memory at bay, then the these promises will likely overlap eachother (multiple of the promises, each holding its own ton of memory), resulting in incredibley high peak memory usage that can cause Out-Of-Memory page crashes. If these promises were to keep these big objects for shorter periods of time, then the times big objects are held in memory would overlap less often, resulting in a lower peak-memory usage and less frequent Out-Of-Memory crashes. In step #9, there are 2 units of memory being taken up. But, let us examine if we put secondFunc inside firstFunc.

fetch("https://www.example.com/index.html")
  .then(function firstFunc(response){
      response.text()
          .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); });
  });

 

  1. {0} On line 1, a Promise object is allocated and returned by fetch on line 1. We will call this promise FetchPromise. {1}
  2. {1} On line 2, function firstFunc is allocated for use in FetchPromise.then method. {2}
  3. {2} On line 2, FetchPromise.then is called with the allocated function firstFunc, returning a Promise we will call ThenPromise1st. However, because ThenPromise1st is never used, it is immediately garbadge collected. {2}
  4. {2} Because there are no longer any references to FetchPromise (e.g. variables assigned to it or methods of it being called), the FetchPromise (the object itself, not the then-functions attatched to it) is garbadge collected and freed from memory. {1}
  5. {1} Waiting for the headers to be recieved...... and still waiting........... then wait some more................................................................................................................... {1}
  6. {1} Now the headers are recieved and FetchPromise is accepted, executing, on line 2, firstFunc and allowing the instance of firstFunc to be garbadge collected because it will never be runned again. {0}
  7. {0} On line 3, while executing firstFunc, a Promise object is allocated and returned by response.text(). We will call this promise HTTPtextPromise because it resolves to the text content of the page at the requested URL. {1}
  8. {1} On line 4, while executing firstFunc, function secondFunc is allocated for use in ThenPromise1st.then method. {2}
  9. {2} On line 4, HTTPtextPromise.then is called with the allocated function secondFunc, returning a Promise we will call ThenPromise2nd. However, because ThenPromise2nd is never used, it is immediately garbadge collected. {2}
  10. {2} On line 4,  HTTPtextPromise.then is internally used up by the browser as this is a case of nesting promises. Then, the  HTTPtextPromise instance is no longer referenced, and is garbadge collected. {1}
  11. {1} Waiting for the body of the HTTP request to be downloaded...... and still waiting........... then wait some more............................................................... {1}
  12. {1} Now that the body of the HTTP request is downloaded, HTTPtextPromise resolves to the downloaded data, triggering ThenPromise2nd to be accepted, executing, on line 4, secondFunc and allowing the instance of secondFunc to be garbadge collected because it will never be runned again. {0}

At step 5, there is 1 less unit of memory being taken up while the headers are being downloaded, making this a more efficient solution. And, yet, we can still greatly optimize this further:

fetch("https://www.example.com/index.html")
  .then(function firstFunc(){
      arguments[0].text()
          .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); });
  });

This will reduce the memory usage further because, in theory, its possible to sneak an evil eval statement into there, and access the local variable response from secondFunc, thus keeping the response variable in memory until secondFunc is garbadge collected. For example, take the following code snippet.

var console = {
    get log(){
        var evaluate = Object.getOwnPropertyNames(window).filter(x=>x[0]==="e"&&x[2]==="a")[0];
        return Math.random() ? window[evaluate] : function(){}; 
    }
};
fetch("https://www.example.com/index.html")
  .then(function firstFunc(response){
      response.text()
          .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); });
      response = null;
  });

 

In theory, the way that eval is accessed could be made infinitely complex, thus forcing browser designers to do silly clingy things to memory because there is no way to 100% predict when a random eval will be quietly injected. However, let us not forget that this optimized code uses only only 1 variable and 1 memory unit less, not millions. Being over obsessive about tiny optimizations like this will not "make-or-break" your javascript code. Rather, what will "make-or-break" your javascript code is just staying mindful of memory management and eliminating major memory problems. Below is an example of such a major memory problem. Assume that only one HTTP request will be made that requires the variable bigArray.

 

var bigArray = [], fromCodePoint = String.fromCodePoint;
for (var i=0, Len=0x10ffff; i !== Len; i++) {
    bigArray[i] = fromCodePoint(i);
}
fetch("https://www.example.com/index.html")
  .then(function firstFunc(){
      arguments[0].text()
          .then(function secondFunc(textContent){ 
                var cpoints = Array.from(textContent).map(char => bigArray[char]);
                console.log("Codepoints: ", cpoints);
          });
  });

During the entire HTTP request, bigArray is hanging around in memory, waiting to be used so it can be garbadge collected. Let us see if we can change that.

fetch("https://www.example.com/index.html")
  .then(function firstFunc(){
      arguments[0].text()
          .then(function secondFunc(textContent){ 
                var bigArray = [], fromCodePoint = String.fromCodePoint;
                for (var i=0, Len=0x10ffff; i !== Len; i++) {
                    bigArray[i] = fromCodePoint(i);
                }
                var cpoints = Array.from(textContent).map(char => bigArray[char]);
                console.log("Codepoints: ", cpoints);
          });
  });

Much better. However, while optimizing the Promise calls, a much more fundamental optimization was skipped over: sometimes, it isn't even neccecary to use massive amounts of memory in the first place. 

 

fetch("https://www.example.com/index.html")
  .then(function firstFunc(){
      arguments[0].text()
          .then(function secondFunc(textContent){ 
                var cpoints = [], cur=0;
                for (var i=0, Len=textContent.length; i < Len; ) {
                    cur = textContent.codePointAt(0);
                    cpoints.push(cur);
                    i += 1 + (cur>0xffff);
                }
                console.log("Codepoints: ", cpoints);
          });
  });

There, now we have eliminated sinister polymorphism and the nasty bigArray variable, making the code snippet several times faster. Sometimes, its easy to overthink certain optimizations while underthinking others. At any rate, it is always best to be knowledgable about the code you are writing.