DEV Community

Cover image for Callbacks vs Promises vs Async/Await: The Ultimate Guide to Asynchronous Javascript
Shubham Tiwari
Shubham Tiwari

Posted on

Callbacks vs Promises vs Async/Await: The Ultimate Guide to Asynchronous Javascript

Learn JavaScript asynchronous programming with callbacks, promises, and async/await. Avoid callback hell and write cleaner, maintainable code with real-world examples.

Hello my fellow frontend developers, today i will be discussing one of the most important concept in javascript, asynchronous programming.

  • I'll be creating my code snippets on Scribbler.live first, which is a fantastic platform that allows you to run a JavaScript Notebook, Online Compiler, and Editor without the need for manual setup.
  • Additionally, I am including a link to code snippets that includes all of the code examples so you can open the snippet and run it yourself to see the results.
  • I will be using scrib.show from scribbler.live, it is equivalent to console.log

Let's dive in

Table of contents

What are callbacks?

  • A callback is a function passed as an argument to another function, which is then executed later (synchronously or asynchronously).
  • Callbacks are fundamental in JavaScript for handling asynchronous operations, event listeners, and functional programming.

Sample codes

Basic example

function saveOrder(orderId, userMail, callback) {
    const order = {
      orderId,
      userMail
    }
    scrib.show(order)
    callback(userMail); // Execute the callback function
}

function sendOrderEmail(userMail) {
    scrib.show(userMail, "Order confirmed")
}

saveOrder("145908275","user@gmail.com", sendOrderEmail);
// Will log the order and sends a confirmation email to the user using sendOrderEmail
Enter fullscreen mode Exit fullscreen mode

Asynchronous callback

  • We could take the example of setTimeout method as it executes asynchronously
console.log("Javascript is awesome");

setTimeout(() => {
    console.log("This codeblock runs after 2 seconds");
}, 2000);

console.log("Scribbler is awesome");

// Output
Javascript is awesome
Scribbler is awesome
This codeblock runs after 2 seconds
Enter fullscreen mode Exit fullscreen mode

Callback Hell (Pyramid of Doom)

function saveOrder(orderId, userMail, callback) {
    const order = {
      orderId,
      userMail
    }
    scrib.show(order)
    callback(userMail); // Execute the callback function
}

function sendDetailsToSeller(orderId, userMail, callback) {
  const details = {
      orderId,
      userMail
    }
    scrib.show(details)
    callback(userMail);
}

function sendOrderEmail(userMail) {
    scrib.show(userMail, "Order confirmed")
}

saveOrder("145908275","user@gmail.com", () => sendDetailsToSeller("145908275","user@gmail.com",sendOrderEmail));
 // If you see we have to pass down the callbacks at multiple level in order to perform chaining operations.
Enter fullscreen mode Exit fullscreen mode
  • When callbacks nest too deeply, they become difficult to read and maintain.

🔴 Problem: Difficult to read, maintain, and debug.
✅ Solution: Promises or Async/Await.

Promises - our saviour from callback hell

  • A Promise in JavaScript represents the eventual success or failure of an asynchronous operation. It provides a cleaner way to handle async tasks compared to traditional callbacks.
  • It takes a function with two parameters:
    • resolve → Call this when the async task is successful.
    • reject → Call this when the async task fails.
  • Promise States: A promise can be in one of three states:
    • Pending → Initial state, neither fulfilled nor rejected.
    • Fulfilled → The operation was successful (resolve() was called).
    • Rejected → The operation failed (reject() was called).

Sample codes

Basic example

const basicPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    let success = true; // Simulating success or failure
    if (success) {
      resolve("Task completed successfully! ✅");
    } else {
      reject("Task failed ❌");
    }
  }, 2000);
});

basicPromise
  .then((result) => {
    scrib.show(result); // Output: Task completed successfully! ✅
  })
  .catch((error) => {
    scrib.show(error); // Output: Task failed ❌ (if failed)
  })
  .finally(() => {
    scrib.show("Promise execution completed.");
  });
Enter fullscreen mode Exit fullscreen mode

Data fetching with fetch method

fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.log("Fetch Error:", error));
Enter fullscreen mode Exit fullscreen mode

Chaining promises methods

// Chaining promises method
new Promise((resolve) => {
  setTimeout(() => resolve(10), 1000);
})
  .then((num) => {
    scrib.show(num); // 10
    return num * 2;
  })
  .then((num) => {
    scrib.show(num); // 20
    return num * 3;
  })
  .then((num) => {
    scrib.show(num); // 60
  });
Enter fullscreen mode Exit fullscreen mode

Promise methods

  • Promise.all - Waits for all promises to resolve. If any promise rejects, the entire result is rejected.
  • Promise.race - Returns the first settled (fulfilled/rejected) promise.
  • Promise.allSettled - Waits for all promises to settle (resolve or reject) and returns their statuses.
  • Promise.any - Returns the first fulfilled promise, ignoring rejections.
const promise1 = new Promise((resolve) => {
  setTimeout(() => resolve(Math.floor(3.14), 1000));
})
const promise2 = new Promise((resolve) => {
  setTimeout(() => resolve(Math.ceil(3.14), 1000));
})
const promise3 = new Promise((resolve) => {
  setTimeout(() => resolve(Math.pow(3.14, 10), 1000));
})

// Promise.all
const promiseAll = Promise.all([promise1,promise2,promise3])
promiseAll.then(scrib.show())

// Promise.race
const promiseRace = Promise.race([promise1,promise2,promise3])
promiseRace.then(scrib.show())

// Promise.allSettled
const promiseAllSettled = Promise.allSettled([promise1,promise2,promise3])
promiseAllSettled.then(scrib.show())

// Promise.any
const promiseAny = Promise.any([promise1,promise2,promise3])
promiseAny.then(scrib.show())
Enter fullscreen mode Exit fullscreen mode
  • For even better async handling, async/await can be used, which simplifies working with promises.

Async/Await

  • async/await is a modern JavaScript feature that simplifies working with asynchronous code. It allows you to write asynchronous code in a synchronous-like manner, making it easier to read, understand, and debug.
  • Why Use async/await?
    • Before async/await, JavaScript developers handled asynchronous code using:
    • Callbacks (led to callback hell)
    • Promises (better, but still required .then() chaining)
  • How async/await works - Declaring a function as async makes it return a promise automatically even if it is returning a simple value.

Sample code

Solving callback hell issue

// Async await example for callback hell issue
async function saveOrder(orderId, userMail, callback) {
    const order = {
      orderId,
      userMail
    }
    scrib.show(order)
    return order
}

async function sendDetailsToSeller(orderId, userMail, callback) {
  const details = {
      orderId,
      userMail
    }
    scrib.show(details)
    return details
}

async function sendOrderEmail(userMail) {
    scrib.show(userMail, "Order confirmed")
    return "Email sent"
}

async function processOrder(orderId, userMail) {
  await saveOrder(orderId, userMail);
  await sendDetailsToSeller(orderId, userMail);
  await sendOrderEmail(userMail);
}

processOrder("145908275","user@gmail.com");
// Much readable and easy to manage
Enter fullscreen mode Exit fullscreen mode

Data fetching

// Fetching data using async/await
async function fetchData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); // ✅ Await fetch

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  const data = await response.json(); // ✅ Await response.json()
  return data;
}

fetchData()
  .then(data => scrib.show("Fetched Data:", data))
  .catch(error =>  scrib.show("Error:", error));
Enter fullscreen mode Exit fullscreen mode

Combining loops with async/await for readable streams

// Combining async/await with loops
async function fetchTodos() {
  let urls = [
    "https://jsonplaceholder.typicode.com/todos/1",
    "https://jsonplaceholder.typicode.com/todos/2",
    "https://jsonplaceholder.typicode.com/todos/3"
  ];

  for (let url of urls) {
    let response = await fetch(url);
    let data = await response.json();
    scrib.show(data);
  }
}

fetchTodos();
Enter fullscreen mode Exit fullscreen mode

Checkout this embed to run the code example mentioned above

That's it for this post, Let me know if i could do any improvements in this article. Also, do check Scribbler.live website.

You can contact me on -

Instagram - https://www.instagram.com/supremacism__shubh/
LinkedIn - https://www.linkedin.com/in/shubham-tiwari-b7544b193/
Email - shubhmtiwri00@gmail.com

You can help me with some donation at the link below Thank you👇👇
https://www.buymeacoffee.com/waaduheck

Also check these posts as well




Top comments (2)

Collapse
 
mahmoudalaskalany profile image
Mahmoud Alaskalany

This makes their understanding very smooth for mr thanks for that , please make one on closures

Collapse
 
gopisuvanam profile image
Gopi Krishna Suvanam

Nice article and very good examples.. makes the concept easy to understand