Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124

Understanding JavaScript Callbacks, Callback Hell, and Promises

JavaScript is a language that heavily relies on asynchronous programming. When dealing with tasks that take time to complete (such as fetching data, file operations, or timers), JavaScript uses callbacks, callback hell, and promises to manage execution flow effectively.

What is a Callback?

A callback is a function passed as an argument to another function, which gets executed later when required. Callbacks allow us to handle asynchronous operations effectively.

Example of a Simple Callback:

setTimeout(()=>{
    console.log('Printing after 5 seconds');
}, 5000);

Here’s a more explicit example of a function accepting a callback:

function hiFunc(param1, func1) {
    console.log('Value of param Passed is ' + param1);
    func1();
}
hiFunc(10, function () {
    console.log('This gets executed afterwards');
});

In this example, func1 is executed after the hiFunc function logs the value of param1.

Using Callbacks for Sequential Execution

Consider an example where we need to create an order, proceed to payment, and then display a confirmation:

function createOrder(product, callback) {
    console.log('Creating an order with', product);
    let createdOrderNumber = Math.floor((Math.random() * 1000) + 1);
    callback(createdOrderNumber);
}
function proceedToPayment(orderNumber, callback) {
    console.log('Moving to Payment Gateway for Order#', orderNumber);
    let createdPaymentNumber = Math.floor((Math.random() * 1000) + 1);
    callback(createdPaymentNumber);
}
function displayConfirmation(paymentNumber, callback) {
    console.log('Payment Successful!! Payment Id #', paymentNumber);
    callback();
}

// Execution
createOrder('Jacket', (param1) => {
    proceedToPayment(param1, (param2) => {
        displayConfirmation(param2, () => {
            console.log('Finally Over');
        });
    });
});

This method ensures sequential execution, but as the number of nested callbacks increases, it leads to callback hell.

What is Callback Hell?

Callback hell refers to deeply nested and hard-to-read callback structures, which make the code difficult to manage and debug.

Example of Callback Hell:

let value = 1;
setTimeout(function () {
    console.log(value++);
    setTimeout(function () {
        console.log(value++);
        setTimeout(function () {
            console.log(value++);
        }, 1000);
        setTimeout(function () {
            console.log(value++);
        }, 1000);
    }, 1000);
}, 1000);

This deeply nested structure makes it hard to maintain and scale. To solve this, we use Promises.

What are Promises?

A Promise is an object representing a future value. It can be in one of three states:

  • Pending: Initial state, before completion or failure.
  • Resolved (Fulfilled): Operation completed successfully.
  • Rejected: Operation failed.

Promises help in writing cleaner, more manageable asynchronous code.

Refactoring Callback Code Using Promises

function createOrder(product) {
    return new Promise(function(resolve, reject) {
        console.log('Creating an order with', product);
        let createdOrderNumber = Math.floor((Math.random() * 1000) + 1);
        setTimeout(function () {
            resolve(createdOrderNumber);
        }, 3000);
    });
}
function proceedToPayment(orderNumber) {
    return new Promise(function(resolve, reject) {
        let createdPaymentNumber = Math.floor((Math.random() * 1000) + 1);
        console.log('Moving to Payment Gateway for Order#', orderNumber, 'with payment number', createdPaymentNumber);
        setTimeout(function () {
            resolve(createdPaymentNumber);
        }, 2000);
    });
}
function displayConfirmation(paymentNumber) {
    return new Promise(function(resolve, reject) {
        console.log('Payment Successful!! Payment Id #', paymentNumber);
        setTimeout(function () {
            resolve();
        }, 1000);
    });
}

// Using Promises for Better Readability
const product = 'Shoes';
createOrder(product)
    .then((orderNumber) => {
        console.log(orderNumber);
        return proceedToPayment(orderNumber);
    })
    .then((paymentNumber) => {
        console.log(paymentNumber);
        return displayConfirmation(paymentNumber);
    })
    .catch((err) => {
        console.log('Error:', err);
    })
    .finally(() => {
        console.log('This is the end statement');
    });

Enhancing with Async/Await

Async/Await makes asynchronous code look more like synchronous code, improving readability.

Refactoring with Async/Await:

async function processOrder(product) {
    try {
        let orderNumber = await createOrder(product);
        console.log(orderNumber);
        let paymentNumber = await proceedToPayment(orderNumber);
        console.log(paymentNumber);
        await displayConfirmation(paymentNumber);
        console.log('Process Completed Successfully');
    } catch (err) {
        console.log('Error:', err);
    } finally {
        console.log('This is the end statement');
    }
}

processOrder('Shoes');

Comparison: Promises vs. Async/Await

FeaturePromisesAsync/Await
ReadabilityModerateHigh
Error Handlingcatch() blockstry/catch blocks
Code StructureChained .then()Linear, sequential style
DebuggingMore complexEasier to debug

Conclusion

  • Callbacks are useful for handling asynchronous operations but can lead to deeply nested structures (callback hell).
  • Callback Hell occurs when multiple nested callbacks make code unreadable and hard to maintain.
  • Promises provide a cleaner and more manageable approach, reducing the nesting issue and making the code more readable.
  • Async/Await simplifies Promise handling, making code more readable and easier to debug.

Understanding and using these concepts effectively will help in writing clean and efficient JavaScript code!

Leave a Reply

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