Introduction:callbacks

Most of the JavaScript actions are asynchronous. To be more precise, you initiate them now, but they finish later. For example, actions like that may be scheduled by setTimeout.

For a better understanding, let’s consider the following example:

function loadScript(src) {
  //creates the <script> tag and adds it to the page, after which
 // the script with the given src starts loading and starts after completion
  let createScript = document.createElement('script');
  createScript.src = src;
  document.head.append(creatScript);
}

In the case above, you can see the loadScript(src) function loading a script with given src.

You can also use the following function:

// load and execute the script at the given path
loadScript('/path/script.js');

You can notice that the action is invoked asynchronously: it starts loading now but runs later when the function is finished.

In case there is a code below loadScript(…), it may not wait till the script loading ends:

loadScript('/path/script.js');
// the code below loadScript
// doesn't wait for the script loading ends

Now, imagine that you want to use the new script as soon as it loads. It is declaring new functions, and you wish to run them. But note that if you do it immediately after the loadScript(…), it will not work. It is demonstrated in the example below:

loadScript('/path/script.js'); // the script has "function newFunction() {…}"
newFunction(); // no such function!

The browser doesn’t have time for loading the script. So, the script loads and runs eventually. That’s all. But you must know when it takes place to apply new functions and variables of that script.

We recommend you to add a callback function in the place of the second argument to the loadScript, which should execute when the script loads.

For instance:

function loadScript(src, callback) {
  let createScript = document.createElement('script');
  createScript.src = src;
  createScript.onload = () => callback(createScript);
  document.head.append(createScript);
}

In case you intend to call new functions from the script, you should write it in the callback, as follows:

loadScript('/path/script.js', function () {
  // the callback runs after the script is loaded
  newFunction();now it works
});

The second argument is considered a function, which runs after the action is completed:

function loadScript(src, callback) {
  let cScript = document.createElement('script');
  cScript.src = src;
  cScript.onload = () => callback(cScript);
  document.head.append(cScript);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', cScript => {
  console.log(`Cool, the script ${cScript.src} is loaded`);
  console.log(_); // function declared in the loaded script
});

It is known as a “callback-based” style of asynchronous programming. A function, doing something asynchronously, needs to provide a callback argument, in which you put the function to run after its completion.

Please, note that it’s a general approach.

Callback Inside Callback

The next question is how to load two scripts sequentially.

Its natural solution is putting the second loadScript call inside the callback, as follows:

loadScript('/path/script.js', function (script) {
  console.log(`Cool, the ${script.src} is loaded, let's load one more`);
  loadScript('/path/script2.js', function (script) {
    console.log(`Cool, the second script is loaded`);
  });
});

After the completion of the outer loadScript, the callback can initiate the inner one. But, you may need another script:

loadScript('/path/script.js', function (script) {
  loadScript('/path/script2.js', function (script) {
    loadScript('/path/script3.js', function (script) {
      // ...continue after all scripts are loaded
    });
  })
});

So, it can be assumed that each new action is inside a callback. It is considered suitable for a few actions, but bad for many.

Handling Errors

In the examples above, errors were not considered.

Let’s check out an enhanced version of the loadScript, which tracks loading errors. It looks like this:

function loadScript(src, callback) {
  let createScript = document.createElement('script');
  createScript.src = src;
  createScript.onload = () => callback(null, createScript);
  createScript.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(createScript);
}

It may call callback(null, script) for a successful load: otherwise there is a callback(error).

Its usage is as follows:

loadScript('/path/script.js', function (error, script) {
  if (error) {
    // handle error
  } else {
    // script loaded successfully
  }
});

The solution, used for the loadScript is still actual. It’s known as “error-first callback” style.

The convention looks like this:

  • The first argument of the callback is kept for the possible error. The callback(err) is called after.
  • The second and the next coming arguments are for the successful result. After that, it is necessary to call callback(null, result1, result2…)).

We can conclude that a callback function can be called for reporting errors, as well as for passing back results.

The Pyramid of Doom

For multiple asynchronous actions following one another, you can use the code below:

loadScript('script1.js', function (error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('script2.js', function (error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('script3.js', function (error, script) {
          if (error) {
            handleError(error);
          } else {
            // continue after loading all scripts
          }
        });
      }
    })
  }
});

As you see, the calls become more nested. Along with them, the code becomes more profound and more challenging to manage, particularly when there is a real code instead of it may contain more loops, conditional statements, and more.

This pyramid of nested calls grows to the right with each of the synchronous actions. Finally, it gets out of control.

It is possible to alleviate such a problem by transforming every action into a standalone function, as follows:

loadScript('script1.js', step1);
function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('script2.js', step2);
  }
}
function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('script3.js', step3);
  }
}
function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // continue after loading all scripts
  }
};

That’s it. It does the same without deep nesting.

There is an additional solution to this problem, which is using JavaScript promise. You can learn about it in the next chapter.




Do you find this helpful?

Related articles