Back to Blog
Tech

Understanding JavaScript Asynchronous Programming Made Easy

From callbacks to Promises to async/await. A step-by-step guide to JavaScript's asynchronous processing.

Mar 4, 20263min read

What Is Asynchronous Programming?

JavaScript is fundamentally a single-threaded language. This means it can only process one task at a time.

However, web applications frequently need to perform time-consuming tasks such as fetching data from a server, setting timers, or reading files. Asynchronous programming is what allows the program to continue doing other work while waiting for these tasks to complete.

Step 1: Callbacks

The most basic approach to asynchronous programming is the callback function.

setTimeout(function() {
  console.log("Executed after 1 second!");
}, 1000);
 
console.log("This runs first.");
// Output: "This runs first."
// After 1 second: "Executed after 1 second!"

Callback Hell

However, when callbacks become nested, the code gets complicated:

fetchUser(userId, function(user) {
  fetchPosts(user.id, function(posts) {
    fetchComments(posts[0].id, function(comments) {
      // The code keeps indenting to the right...
      console.log(comments);
    });
  });
});

This is known as "Callback Hell."

Step 2: Promises

Promises were introduced to solve callback hell.

fetch('/api/user/1')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error('Error:', error));

A Promise has three states:

  • Pending: No result yet
  • Fulfilled: The operation completed successfully
  • Rejected: The operation failed

Step 3: async/await

Introduced in ES2017, async/await makes asynchronous code read like synchronous code.

async function loadUserPosts(userId) {
  try {
    const userResponse = await fetch(`/api/user/${userId}`);
    const user = await userResponse.json();
 
    const postsResponse = await fetch(`/api/posts/${user.id}`);
    const posts = await postsResponse.json();
 
    return posts;
  } catch (error) {
    console.error('Failed to load data:', error);
  }
}

Much easier to read, right?

Important Note

await can only be used inside an async function. Don't forget the async keyword before the function!

Parallel Execution: Promise.all()

When you want to run multiple asynchronous operations simultaneously, use Promise.all().

async function loadDashboard() {
  // Sequential execution (slower)
  // const user = await fetchUser();
  // const posts = await fetchPosts();
  // const stats = await fetchStats();
 
  // Parallel execution (faster)
  const [user, posts, stats] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchStats()
  ]);
 
  return { user, posts, stats };
}

Summary

ApproachProsCons
CallbacksSimple and intuitiveGets complex when nested
PromisesImproved readability with chainingStill repetitive then/catch
async/awaitReads like synchronous codeCheck browser support

In Closing

Asynchronous programming may feel difficult at first, but understanding the progression from callbacks to Promises to async/await makes it click naturally. In particular, async/await is an essential concept in modern JavaScript development, so make sure to master it!

Get new posts by email ✉️

We'll notify you when new posts are published