All about callback, callback hell and Promise in JavaScript

·

6 min read

In order to understand everything about asynchronous programming, we will first see what is synchronous programming, and, why there is need to have the concept call asynchronous programming in JavaScript. Lets begin.....

Most programming languages executes the code in top-to-bottom manner, which means executing the code line by line, one after the other. And, if some function has dependency on another function, then the rest of the code has to wait until that dependent function has completely executed. This means, rest of the program will be halted until the task taking longer time finishes its execution. This is what we call Synchronous programming.

Below is the example on synchronous program-

const city= 'Delhi';
const address = `Hello, I am from ${city}!`;
console.log(address );

// "Hello, I am from Delhi!"

Here, we can see the the code is executed sequentially, waiting for the previous line to run, and, then proceeds to the next line.

From here arose the concept of asynchronous programming- In this, instead of being halted because of the functions or methods which takes longer time to execute, asynchronous programming can move to other task before the previous one finishes

In the next few minutes, we are going to cover the below concepts, as these are the utmost important topics of asynchronous programming world.

  • Why we need asynchronous programming?
  • What is callback?
  • How we can escape from the problem of callback hell, using Promise?
  • Why async await? When we already have promises at place ?

Why we need asynchronous programming? The reason code is being synchronously executed is because JavaScript is a single threaded language, and a thread can perform a single task at a time. So, to overcome this problem we have asynchronous programming.

In asynchronous programming we are able to deal with several tasks simultaneously without even blocking the rest of the code. Hence, we are able to complete more tasks in shorter period as they are being run parallelly.

The way to achieve asynchronicity in JavaScript is by using callbacks. Now you must be wondering what is that, right? Lets cover that as well..

What is callback?

There are many functions or the set of APIs provided by browser that allow us to perform asynchronous actions. Suppose you want to perform some set of actions only when user clicks the button, but we don't know beforehand when user is going to click a button. So, we can define an event handler for that button and pass a function to it which will perform the actions when the button is clicked.

document.getElementById('progress-button').addEventListener('click', () => {
    console.log("item clicked");
});

The function we have passed as second argument inside another function is a callback. It will be executed only when the event passed in first argument has triggered.

Let's take one more example to understand it more clearly, suppose are program reads the content from the file and will display its content only when the file has been read.

readFile("content.txt", function(err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});

The readfile() reads from a file(content.txt, the first argument) and once it's completed, it executes the callback(the second argument). So , now comes the question what if some error is encountered in callback, how to handle that? For that, the first parameter in any callback function is the error object and the second argument are foe successful result.

If there is some error, the information about it will be stored in error object, if not, then the object is null.

Now, lets imagine you want to load the script in the browser using some asynchronous load function , like below-

//Asynchronous load function
function loadScript (src) {
    let script = document.createElement('script')
    script.src = src
    document.head.append(script)
 }

loadScript('./1.js')

At first it might look good approach to achieve the above scenario, and no doubt it is. For only one or two level deep nested calls it looks fine, but for multiple asynchronous options that comes one after the, for example if we want to load the other scripts, only when the previous ones are loaded, code will look like below,

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

loadScript('./1.js', function (script) {
    console.log(script)
    loadScript('./2.js', function (script) {
        console.log(script)
        loadScript('./3.js', function (script) {
            console.log(script)
            //...
        })
    })
})

As you can see the more nested the code is becoming to achieve the asynchronous task, the more difficult it is becoming to read and maintain. And, this is what we call as "callback hell" or "pyramid of doom".

How we can escape from the problem of callback hell, using Promise?

As the heading suggests we will look into the solution for callback hell using Promise. Now, before heading towards the solution lets first discuss, what is Promise?

The Promise is an object which helps in working with asynchronous operations. It represent the eventual completion (or failure) of an asynchronous operation, and its resulting value. It helps in resolving the issues with multiple callbacks and also helps in providing a better way to deal with error or success conditions.

It is used to overcome issues with multiple callbacks and provides a better way to manage success and error conditions.

let promise = new Promise(function(resolve, reject) {
     if (asychronous operation) { 
        resolve('Promise resolved!');
    }
    reject('Promise  rejected!');
});

It is the constructor syntax for promise object, and the function which is passed in the Promise constructor is executed as soon as the Promise is created. This function accepts takes two argument resolve and reject which are callbacks. When the executor is resolved successfully resolve() is called else reject() will be called.

The promise returned by the Promise constructor has below three different states of execution of the asynchronous code -

  • Pending : Initially, the state is pending which means asynchronous operation hasn't completed yet.

  • Fulfilled : When the asynchronous operation has completed successfully, when resolve is called.

  • Rejected : When the asynchronous operation has failed due to some error, when reject is called.

With help of Promise chaining we can get rid of callback hell, lets convert the above callback hell solution into Promise-

function loadScript (src) {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        script.src = src
        script.onload = () => resolve(src) //fulfilled,result
        script.onerror = (err) => reject(err) //rejected,error
        document.head.append(script)
    })
}

loadScript('./1.js')
    .then(() => {
        return loadScript('./2.js')
    }, (err) => {
        console.log(err)
    }).then(() => {
        return loadScript('./3.js')
    }, (err) => {
        console.log(err)
    })

So, from all the above discussion we can conclude that code has become shorter and cleaner with promises as we got rid of callback hell . Hope this blog helped you in some way in understanding asynchronous programming. Happy reading.