Callback, Promise and Async/Await in JavaScript

Callback, Promise and Async/Await in JavaScript

Callback, Promise and Async/Await are the ways in which asynchonous operation is done in JavaScript. In this post we will start by understanding the synchronous execution in JavaScript.

Then we will understand the process of asynchronous execution using Callback, Promise and Async/Await in JavaScript. We will understand the syntax of Callback, Promise and asyc/await with the example of a code snippet.

Understanding the synchronous execution in JavaScript

JavaScript is a single threaded language and it works with the data structures like stack, queue and heap.

All the user actions or events has a message associated with them. When an event like a click event occurs a message (i.e the function that needs to be called on click) is first sent to the queue . There is a event loop between the queue and a stack. If the stack is empty the message is dequeued from the queue and pushed into the stack.

The stack is responsible for executing the code. Once the code is executed entirely another message is picked up by the event loop from the queue and pushed to the stack.

When there are lots of events triggered those are all lined up in the message queue and picked up by the event loop and executed sequentially.

This approach of execution is called synchronous apporoach. There is a wait time in processing the messages present in the queue. The next message cannot be picked up from the queue for processing until the current message is processed entirely.

This is the reason why we sometimes see the message 'The script you are executing is taking longer than expected to run. Click End to abort the script. or Continue to continue script execution.'

Asynchronus execution in JavaScript

We have seen the synchronous behaviour of JavaScript above. Synchronous execution would not be the right thing to do when in several scenarios.

One such scenario would be doing an I/O operation from the network like fetching data from database or streaming a video etc...

Since such I/O operation takes time to complete, all the messages in the queue would be in a waiting state giving a bad experience to the user.

That is where comes the role of asynchronous execution. In addition to the Stack, Queue and Heap there are also browser APIs that keep a track of callback function to be executed after the completion of the asynchronous operation and then let the stack continue with the execution of other messages from the queue.

Once the asynchronous operation is completed the APIs adds the callback function to the queue to be picked up by the event loop for execution in the stack.

Callback

A callback is a function that is passed as an arguement to another function. This is basically done to achieve asynchonous behaviour in JavaScript.

An important point to note here is that not all functions that take another function as an arguement is a callback function. It completely depends on how the callback function is called inside the containing function.
If the callback function is called inside the code block of asynchronous call it gets executed asynchronously.

Lets consider the below mentioned examples to better understand synchronous and asynchonous callbacks.

//Synchronous callback
const doSomeWork = (success, error) => {
  console.log("Do some work");
  try {
    //do something
    success();
  } catch (err) {
    error();
  }
  console.log("do another stuff");
};

const success = () => {
  console.log("Work completed successfully");
};
const error = () => {
  console.log("Work completed with errors");
};

doSomeWork(success, error);

Output
Do some work
Work completed successfully
do another stuff

The above code is an example of synchronous callback and the statements would execute sequentially.


//Asynchronous callback
const doSomeWork = (success, error) => {
  console.log("Do some work");
  setTimeout(() => {
    try {
      //do something
      success();
    } catch (err) {
      error();
    }
  }, 5000);
  console.log("do another stuff");
};

Output
Do some work
do another stuff
Work completed successfully

As seen in the output above the execution continues ahead without waiting for the asynchronous call to get completed.

Promise

As seen in the above code the callback pretty much does the job asynchronously but what if we need to perform multiple operations such that an operation can only start after the completion of previous operation. We may end making nested callback which in a practical scenarios becomes very complex to handle and this is wat is called a callback hell or a pyramid of doom.

//Callback hell
const files = [
  { name: "file1" },
  { name: "file2" },
  { name: "file3" },
  { name: "file4" },
];
const readFile = (file, callback) => {
  setTimeout(() => {
    console.log("processing ...", file.name);
    callback(file);
  }, 2000);
};
//Callback hell
readFile(files[0], function (file) {
  readFile(files[1], function (file) {
    readFile(files[2], function (file) {
      readFile(files[3], function (file) {});
    });
  });
});

To avoid a pyramid of doom or callback hell we use Promise or async/await. Let us understand Promise in general terms first and then we will go through it technically. In real life when we make Promise for something there can be two things that can happen in future-

  1. We either keep the promise we had made by doing what was promised
  2. We do not keep the promise and we fail to do what was promised.

Similarly in JavaScript a Promise is an object which after a certain amount of time is either resolved by completing the task successfully or it rejected if the task is not completed successfully.
So a promise has two important functions resolve() and reject(). There are steps in the Promise -

  1. Creation of a Promise

    const doSomeWork = () => {
      console.log("Do some work");
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          try {
            //code to do some work
            resolve("Work completed successfully");
          } catch (err) {
            reject("Work completed with errors");
          }
        }, 2000);
      });
    };
    
  2. Using the Promise

doSomeWork()
  .then((res) => {
    console.log(res);
  })
  .catch((err) => console.log(err));

Mostly in all the front end and backend development we rarely create a promise but more often deal with using a Promise.
When a promise is completed successfully the value is returned from the resolve() method and it is received in the 'then' block while using the promise.
When a promise is not completed successfully due to some errors then the error is passed from the reject() method and it is caught in the catch block of the promise.

Promise have three states -

  1. Pending - This is the initial state of the promise before the beginning of an operation or when the promise is created.
  2. Fulfilled - This means that the specified operation was completed successfully.
  3. Rejected - This means that the operation did not completed successfully and a error is returned.

Promise have much more functionality and you can refer to the official documentation for the details. Promise

Async/Await

Async/Await is a syntactic sugar to the way of writing promises in a cleaner way. Async keyword indicates that a method is an asynchronous method and an await keyword is always written inside an async block however an async function can exist without having the await keyword.

const doSomethingAsync = async () => {
  console.log("doSomethingAsync : inside doSomethingAsync");
  const result = await doSomeWork();
  console.log(result);
  console.log("doSomethingAsync : continue with other work");
};
const doSomething = () => {
  console.log("doSomething : inside doSomething");
  const result = doSomeWork();
  console.log(result);
  console.log("doSomething : continue with other work");
};
const doSomeWork = () => {
  return new Promise((resolve, reject) => {
    console.log("work in progress");
    setTimeout(() => {
      try {
        resolve("work completed successfully");
      } catch (err) {
        reject("work completed with error");
      }
    }, 5000);
  });
};

doSomething();
doSomethingAsync();

In the above mentioned code there are two versions of the doSomething method doSomething() and doSomethingAsync() . doSomethingAsync() is an async version with an await operation. The async keyword just indicates that the method doSomeWorkAsync() is an asynchronous operation however the await keyword actually converts the async operation to a synchronous operation and the next statement does not get executed unit the doSomeWork() method has completed its execution.

In the doSomething() version the execution of code continues with the execution of next statement without even waiting for the doSomeWork() method to get completed.

I hope I was able to explain the basics of Callback, Promise and Async/Await and the concept of synchronous and asynchronous programming in JavaScript.
You can refer to the code from my github repository - [CallbackPromiseAsyncAwait Example]((https://github.com/abyshekhar/CallbackPromiseAsyncAwaitExample)

Book Recommendation

I would recommend you to refer to the book JavaScript - The Definitive Guide by David Flanagan for an in-depth understanding of the JavaScript Language.