Skip to main content

Promises and promise chaining

Time before promise, callbacks

There are entities in JavaScript that let you schedule asynchronous tasks, tasks that you initiate first, then they will finish later.

Task like disk reading, fetching resources from network that will take arbitrary amount of time and you don't want your CPU to sit at that line of code waiting until it finishes. You would like to schedule it and then come back when the task finishes. Functions such as setTimeout, setInterval, or asynchronous file reading let you do that, let you schedule an asynchronous task, they start, but finishes later, when the resources that they are waiting on are finished.

Often time, after the task is finished, we want to use the result of the task immediately how would we do that? We cannot just add a function call right after the task because it will be scheduled later, and we don't know if it will finishes right after it starts.

Say loadScript will take 10 seconds to load, then calling newFunction() immediately after you started loading will be an error.

loadScript('myscript.js'); // contains newFunction(), but takes 10 seconds to load

newFunction(); // no such function
Introducing callbacks

A callback function is a function that will be run after the asynchronous task is finished. We can add a callback parameter to the loadScript function

function loadScript(src, callback) {
	let script = document.createElement('script');
  	script.src = src;
  	script.onload = () => callback(script); // Scripts are loaded asynchronously by the browser.
  											// We have to provide an empty arrow function because the
  											// event handler for onload calls a function with no parameter
  											// inside the body we will invoke the callback, when script is loaded
  	document.head.append(script);
}

loadScript('myscript.js', (script) => console.load(`My script ${script} is loaded`));

Okay, then this guarantees that "My script ${script} is loaded" message to show up after myscript.js is loaded into the HTML. Now here comes the interesting part, what if I want to load a second script myscript2.js right after myscript.js is finished?

Easy, we can just add another loadScript function call inside the callback of the first script load.

loadScript('myscript.js', function(script) {
	console.log("First script loaded");
  
 	loadScript('myscript2.js', function(script) {
      	console.log("Second script loaded");
    });
});

And what if there is a third script that I want to load only after myscript2.js finishes? We would just continue on nesting into the callbacks, and this is what is called the Pyramid of Doom or callback hell.

It gets worse if you added error handling into loadScript itself, what if the script that you are loading was unable to complete? It adds another layer or nesting like so:

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continue after all scripts are loaded (*)
          }
        });

      }
    });
  }
});
Solution

This can be partially alleviated by storing each step of script loading into a function on it's own, but then it creates a function that will only be called once. The better solution is to use Promise