asynchronous javascript

Understanding Asynchronous JavaScript

When I was trying to learn programming and the Javascript language to be specific, one of the main pain points was understanding what asynchronous programming was and why we needed it. If you are anything like me, I will slowly take you through the concept of asynchronous programming and explain some of the concepts in a way that you will understand.

what is asynchronous programming and why do we need it?

When using an application, one of the things that a programmer should keep in mind is responsiveness. If your app ‘hangs’ when one operation is running and the user has to wait until that operation is done to do something else in your app, then there is a problem.

Simple functions are usually synchronous in nature, meaning the code is read from top to bottom. If you are writing a web application and have a simple function like 

const name = "Wanjiru";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Wanjiru!"

The browser reads each line of code, waits for the line to finish its work and then goes to the next line. Another way to write the above function would be to separate the functions as shown below:


function makeGreeting(name) {
return `Hello, my name is ${name}!`;
}

const name = "Wanjiru";
const greeting = makeGreeting(name);
console.log(greeting);
// "Hello, my name is Wanjiru!"


In the above example, the function is still synchronous, since the caller has to wait for the execution of the makeGreeting() function before it can continue. While this is good for an app that has a simple function like the one I wrote above, this may be really ineffective when trying to for example make HTTP requests using fetch(). This is where the asynchronous functions come in. Let’s take a look at another example:

function makeGreeting(name) {
  return `Hello, my name is ${name}!`;
}

function greetingResponse(name) {
  return `Nice to meet you ${name}!`;
}

const name = "Wanjiru";
const greeting = makeGreeting(name);
const response = greetingResponse(name);
console.log(greeting);
// "Hello, my name is Wanjiru!"
for(let i=0;i<1000000000;i++){
}
console.log(response); 
//”Nice to meet you Wanjiru”

If you run the above code, you will realize that the greeting will log and wait for a few minutes before the response is logged on the console. This is because the loop between the two function calls has to be completed before the response function can be called and logged in the console. With asynchronous programming, we can solve this problem.

Asynchronous functions are functions that run in parallel with other functions. With asynchronous code, multiple code can execute, while other tasks finish in the background. This is also referred to as non blocking code. A good example of an asynchronous function is the javascript’s setTimeout function. Let’s take a look at the following example:

let greeting = "Hello Wanjiru, how is your day going?"
let response = "Hello to you, my day is going great!"
console.log(greeting)
setTimeout(function(){
    console.log("This is an asynchronous function.");
}, 10000)
console.log(response); 

If you run the above code, you will notice that the greeting will log and shortly after the response will log even if there is a line of code that has to wait for ten seconds to log. After the 10 seconds that we set in our setTimeout function,the line “This is an asynchronous function.” will log.

Understanding Callbacks and how they work.

A callback is a function that is passed as an argument for another function. Lets look at an example that I found really helpful in understanding why people use callbacks.

Let’s assume we are trying to build a simple calculator. Below is my code for the calculator. 

Function compute(operation, x, y){
If (operation === “add”)
  {
	return x + y
  }

else if (operation === “subtract”)
  {
	return x - y
  }
}
console.log( compute(“add”, 7,3) ) // result is 10
console.log( compute(“subtract”, 7,3) ) // result is 4


If we keep going with all the operations that a calculator should handle, then we will have a long chain of elseif statements. This is where callbacks come in. Let’s remodel our calculator function by using callbacks.

function add(x,y){
    return x+y
}

function subtract(x,y){
    return x-y
}

function compute(callBack, x, y){
    return callBack(x,y)
}

console.log(compute(add, 7, 5))    // 12
console.log(compute(subtract, 10, 4)) //6

In the above example,  when we call compute with three arguments, one of them is an operation. When we enter in the compute function, the function returns a function with a given action name. It, in response, calls that function and returns the result.

As in with the previous elseif statements, you realize that if you have multiple functions called inside another callback, many levels deep, it can quickly spiral into unreadable code that is very hard to maintain and debug. This is famously known as a callback hell.Here is an  example of a callback hell for Fetching User Data, Posts, and Comments

function fetchUser(userId, callback) {
  setTimeout(() => {
    console.log(`Fetched user with ID: ${userId}`);
    callback(null, { id: userId, name: "John Doe" });
  }, 1000);
}

function fetchPosts(userId, callback) {
  setTimeout(() => {
    console.log(`Fetched posts for user ID: ${userId}`);
    callback(null, [
      { postId: 1, title: "Post 1" },
      { postId: 2, title: "Post 2" },
    ]);
  }, 1000);
}

function fetchComments(postId, callback) {
  setTimeout(() => {
    console.log(`Fetched comments for post ID: ${postId}`);
    callback(null, [
      { commentId: 101, content: "Great post!" },
      { commentId: 102, content: "Thanks for sharing." },
    ]);
  }, 1000);
}

// Callback hell begins here
fetchUser(1, (err, user) => {
  if (err) {
    console.error("Error fetching user:", err);
    return;
  }

  fetchPosts(user.id, (err, posts) => {
    if (err) {
      console.error("Error fetching posts:", err);
      return;
    }

    fetchComments(posts[0].postId, (err, comments) => {
      if (err) {
        console.error("Error fetching comments:", err);
        return;
      }

      console.log("User:", user);
      console.log("Posts:", posts);
      console.log("Comments on first post:", comments);
    });
  });
});

What Makes It “Callback Hell”?

  1. Deep Nesting:
    • Each asynchronous function depends on the result of the previous one, creating a pyramid of doom with increasing indentation.
    • This makes the code harder to read and maintain.
  2. Error Handling:
    • Errors must be handled at every level of the callback chain, increasing complexity.
  3. Scalability:
    • Adding more steps (e.g., fetching likes, followers, etc.) makes the nesting worse and the code even harder to follow.

How do we avoid callback hell you may ask, we use something called promises. A promise is a placeholder for the future result of an asynchronous operation. In simple words, we can say it is a container for a future value. 

A javascript promise object can either be pending, fulfilled or rejected. The Promise object supports two properties: state and result.
While a Promise object is “pending” (working), the result is undefined.

When a Promise object is “fulfilled”, the result is a value.

When a Promise object is “rejected”, the result is an error object.



myPromise.state


myPromise.result


“pending”
undefined


“fulfilled”


a result value


“rejected”


an error object

You cannot access either the promise state or promise result, you need to use a promise method to handle promises.

When using promises, we do not have to rely on callback, therefore, avoiding the callback hell. Let’s remodel our previous code example to use promises. 

function fetchUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Fetched user with ID: ${userId}`);
      resolve({ id: userId, name: "John Doe" });
    }, 1000);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Fetched posts for user ID: ${userId}`);
      resolve([
        { postId: 1, title: "Post 1" },
        { postId: 2, title: "Post 2" },
      ]);
    }, 1000);
  });
}

function fetchComments(postId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Fetched comments for post ID: ${postId}`);
      resolve([
        { commentId: 101, content: "Great post!" },
        { commentId: 102, content: "Thanks for sharing." },
      ]);
    }, 1000);
  });
}

// Chaining Promises
fetchUser(1)
  .then((user) => {
    return fetchPosts(user.id).then((posts) => ({ user, posts }));
  })
  .then(({ user, posts }) => {
    return fetchComments(posts[0].postId).then((comments) => ({
      user,
      posts,
      comments,
    }));
  })
  .then(({ user, posts, comments }) => {
    console.log("User:", user);
    console.log("Posts:", posts);
    console.log("Comments on first post:", comments);
  })
  .catch((err) => {
    console.error("Error:", err);
  });

When a promise is created it runs asynchronously. When the given task is completed, then we say the promise is settled. After the promise is settled, we can have either a fulfilled or rejected promise state based on the outcome of the promise. We can handle these different states in different ways in our code. “Producing code” is code that can take some time

“Consuming code” is code that must wait for the result

A Promise is an Object that links Producing code and Consuming code. It contains both the producing and the consuming code.

 The .then() method shown in the code above is used as a way of consuming promises.  There are other methods for promises like catch(), which help in catching an error in from the rejected promise.

You can create a new promise and make any task asynchronous by using new promise() method. 

Another way of consuming promises is by using the async/await methods. Using the then() method on promises can quickly get messy. The async/await method of consuming promises uses .then() method behind the scenes. If you want to handle asynchronous tasks in your functions, you have to make that function asynchronous using the async keyword before the function. Wherever promises are returned we have to use await before it to consume promises. Here is how we can improve our previous code example using async/await.

async function main() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].postId);

    console.log("User:", user);
    console.log("Posts:", posts);
    console.log("Comments on first post:", comments);
  } catch (err) {
    console.error("Error:", err);
  }
}

main();

As you can probably see, using the async/await simplifies the code and makes it more readable and easy to maintain. 

This is all we will talk about in this article. I hope you better understand asynchronous programming now than when you started reading. Happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *