JavaScript Async: Callbacks, Promises & Async/Await - Featured Image
Web development3 min read

JavaScript Async: Callbacks, Promises & Async/Await

Asynchronous programming is one of the most crucial concepts in JavaScript, especially for frontend developers. Whether you're fetching data, handling events, or executing delayed operations, understanding async programming helps you write cleaner and more efficient code. In this blog, we'll explore three key approaches to handling asynchronous code in JavaScript: callbacks, promises, and async/await. We'll also discuss common pitfalls like callback hell and how modern techniques make async programming easier to manage.

Understanding callbacks

A callback is simply a function passed as an argument to another function, which is then executed later. Callbacks are fundamental in JavaScript for handling asynchronous tasks, event listeners, and functional programming.

Basic callback example

function saveOrder(orderId, userEmail, callback) {
    const order = { orderId, userEmail };
    console.log(order);
    callback(userEmail); // Execute the callback function
}

function sendOrderEmail(userEmail) {
    console.log(`Order confirmed for ${userEmail}`);
}

saveOrder("145908275", "user@gmail.com", sendOrderEmail);

Asynchronous callback example

console.log("JavaScript is awesome");

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

console.log("Scribbler is awesome");

Output:

JavaScript is awesome
Scribbler is awesome
This code runs after 2 seconds

Callback hell

When callbacks are nested too deeply, they become difficult to read and maintain.

function saveOrder(orderId, userEmail, callback) {
    const order = { orderId, userEmail };
    console.log(order);
    callback(userEmail);
}

function sendDetailsToSeller(orderId, userEmail, callback) {
    const details = { orderId, userEmail };
    console.log(details);
    callback(userEmail);
}

function sendOrderEmail(userEmail) {
    console.log(`Order confirmed for ${userEmail}`);
}

saveOrder("145908275", "user@gmail.com", () =>
    sendDetailsToSeller("145908275", "user@gmail.com", sendOrderEmail)
);

Problem: Nested callbacks make the code harder to read and debug.

Solution: Use Promises or Async/Await.

Promises

A Promise represents the eventual success or failure of an asynchronous operation. It helps avoid callback hell by allowing us to chain operations more clearly.

Basic promise example

const basicPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        let success = true;
        success ? resolve("Task completed successfully! ✅") : reject("Task failed ❌");
    }, 2000);
});

basicPromise
    .then((result) => console.log(result))
    .catch((error) => console.log(error))
    .finally(() => console.log("Promise execution completed."));

Fetching data with promises

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));

Promise chaining

new Promise((resolve) => {
    setTimeout(() => resolve(10), 1000);
})
    .then(num => {
        console.log(num); // 10
        return num * 2;
    })
    .then(num => {
        console.log(num); // 20
        return num * 3;
    })
    .then(num => console.log(num)); // 60

Promise methods

  • Promise.all - Resolves when all promises are resolved (fails if any reject).

  • Promise.race - Resolves when the first promise resolves or rejects.

  • Promise.allSettled - Waits for all promises to complete and returns results.

  • Promise.any - Resolves with the first fulfilled promise (ignores failures).

const promise1 = new Promise(resolve => setTimeout(() => resolve(3), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve(4), 1000));
const promise3 = new Promise(resolve => setTimeout(() => resolve(5), 1000));

Promise.all([promise1, promise2, promise3]).then(console.log); // [3, 4, 5]
Promise.race([promise1, promise2, promise3]).then(console.log); // First resolved value
Promise.allSettled([promise1, promise2, promise3]).then(console.log);
Promise.any([promise1, promise2, promise3]).then(console.log);

Async/Await

Async/Await simplifies working with promises by allowing us to write asynchronous code in a synchronous-like manner.

Why use Async/Await?

  • More readable than chaining .then()

  • Handles errors better with try/catch

  • Easier to debug

Solving callback hell with Async/Await

async function saveOrder(orderId, userEmail) {
    const order = { orderId, userEmail };
    console.log(order);
    return order;
}

async function sendDetailsToSeller(orderId, userEmail) {
    const details = { orderId, userEmail };
    console.log(details);
    return details;
}

async function sendOrderEmail(userEmail) {
    console.log(`Order confirmed for ${userEmail}`);
    return "Email sent";
}

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

processOrder("145908275", "user@gmail.com");

Fetching data with Async/Await

async function fetchData() {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return await response.json();
}

fetchData()
    .then(data => console.log("Fetched Data:", data))
    .catch(error => console.log("Error:", error));

Async/Await with loops

async function fetchTodos() {
    const 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();
        console.log(data);
    }
}

fetchTodos();

Conclusion

By understanding callbacks, promises, and async/await, you can write asynchronous JavaScript code that is clean, readable, and efficient. Async/Await is the recommended approach in modern JavaScript, but knowing Promises and Callbacks helps you work with legacy code and APIs.

hassaankhan789@gmail.com

Frontend Web Developer

Posted by





Subscribe to our newsletter

Join 2,000+ subscribers

Stay in the loop with everything you need to know.

We care about your data in our privacy policy

Background shadow leftBackground shadow right

Have something to share?

Write on the platform and dummy copy content

Be Part of Something Big

Shifters, a developer-first community platform, is launching soon with all the features. Don't miss out on day one access. Join the waitlist: