Asynchronous JavaScript - A Practical Guide to Callbacks, Promises, and Async/Await

Asynchronous JavaScript - A Practical Guide to Callbacks, Promises, and Async/Await

Have you ever struggled to navigate a website because it loaded slowly? This means that the website was not responsive.

To address such issues, one of the methods developers incorporate into their coding practice is asynchronous programming.

In this article, I will dive deep into the practical aspects of asynchronous Javascript (AJAX), exploring topics such as promises, callbacks, and async/await.

What is Asynchronous programming in Javascript?

Asynchronous programming in Javascript is a technology that enables a program to skip to another task while processing the previous task in the background without interfering with the display of a webpage.

By default, Javascript is single-threaded and follows the synchronous programming pattern, executing tasks sequentially, one code block at a time.

Therefore, when a certain function requires more time to complete, the next line of code will not commence processing until the former task concludes.

Unlike this model, asynchronous programming makes it possible for Javascript to advance to the next block of code while waiting for the response from the previous code.

For example, developers can request data from external APIs while other tasks run simultaneously. This, in turn, ensures that the user interface remains responsive.

Why is Asynchronous programming important?

Apart from improving the responsiveness of applications, asynchronous programming also enables better utilization of system resources.

While one part of the program waits for an operation to complete, other parts can continue executing. Consequently, this reduces the idle time of the CPU and maximizes resource usage.

Additionally, this programming model is crucial for building scalable systems, especially where a large number of tasks are involved.

Also, in web development, asynchronous Javascript(AJAX) allows web pages to load dynamically without requiring a full page reload. Thereby boosting user experience.

Lastly, asynchronous programming is best used in building real-time applications such as financial trading or gaming platforms where timely processing is critical.

JavaScript callbacks

In JavaScript, a callback is a function that is passed as an argument into another function and is executed after the completion of a particular operation.

Consider a scenario where you want an action to take place after a specific task is completed.

Instead of waiting for the operation to finish, you can introduce a callback function to execute once the task is done.

Callbacks are best used in event-driven programming, where functions are triggered in response to events like user actions.

Additionally, it promotes modularity and allows you to properly organize code in a readable format.

Asynchronous callback:

// Asynchronous function with a callback
function performTaskAsync(callback) {
  console.log("Task started");
  // Simulating an asynchronous operation using setTimeout
  setTimeout(function () {
    console.log("Task completed");
    // Execute the callback function
    callback();
  }, 2000); // Simulating a 2-second delay
}
// Callback function
function callbackFunction() {
  console.log("Callback executed!");
}
// Using the asynchronous callback
performTaskAsync(callbackFunction);

In this example, performTaskAsync simulates an asynchronous task using setTimeout. The callback function is executed after the asynchronous task is completed.

Common use cases of callbacks

Callbacks are commonly used in the following scenarios:

  1. Event handling: this is a type of asynchronous programming that incorporates the callback function.

     // Event listener with a callback
     document.getElementById("myButton").addEventListener("click", function () {
       console.log("Button clicked!");
     });
    
  2. AJAX requests: An AJAX request allows developers to send and receive data from a server asynchronously, without having to reload the entire web page. It also uses callbacks.

    In traditional AJAX, the XMLHttpRequest object is used to make requests to the server. However, modern web development often uses the fetch API, which provides a more flexible and powerful way to handle HTTP requests.

     // AJAX request with a callback
     function fetchData(url, callback) {
       // Simulating an AJAX call
       fetch(url)
         .then(response => response.json())
         .then(data => callback(data))
         .catch(error => console.error(error));
     }
     // Callback function
     function handleData(data) {
       console.log("Data received:", data);
     }
     // Using the fetchData function
     fetchData("https://api.example.com/data", handleData);
    

Callback hell

While callbacks are powerful, they can lead to a phenomenon known as "callback hell" or "pyramid of doom." This occurs when multiple asynchronous operations are nested within one another, resulting in deeply indented and hard-to-read code.

getUserData((userData) => {
    getAdditionalInfo(userData, (additionalInfo) => {
        processInfo(userData, additionalInfo, (result) => {
            // More nested callbacks...
        });
    });
});

To prevent a callback hell:

  • Break down your code into smaller manageable pieces.

  • Implement proper error handling within callbacks.

  • Define named functions to promote usability.

  • Use control flow libraries like promises.

Javascript promises

Promises are a way to handle asynchronous operations more elegantly, compared to callbacks. A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value.

It addresses the callback hell issue by improving the flow of the code and also making the code more readable.

Creating and using promises

A promise can be created by using the Promise constructor, which takes a function with the parameters resolve and reject.

The resolve function is called when the asynchronous operation is successful, and reject is called when there's an error.

//creating a promise
const myPromise = new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve("Operation successful!");
        } else {
            reject("Oops! Something went wrong.");
        }
    }, 1000);
});

// Using the promise
myPromise
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.error(error);
    });

Chaining promises

Promises allow chaining, making it easier to manage multiple asynchronous operations sequentially. This is achieved using the then method, which returns a new promise.

const firstPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve("First operation completed");
    }, 1000);
});

const secondPromise = (message) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`${message}, Second operation completed`);
        }, 1000);
    });
};

// Chaining promises
firstPromise
    .then((result) => {
        console.log(result);
        return secondPromise(result);
    })
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.error(error);
    });

Handling errors with promises

Errors in promises are handled using the catch method, which allows you to centralize error handling for all promises in a chain.

const errorPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = false;
        if (success) {
            resolve("Operation successful!");
        } else {
            reject("Oops! Something went wrong.");
        }
    }, 1000);
});

errorPromise
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.error(error); // Will catch the error from the promise
    });

Promises terminology

There are only two properties used for Javascript promises - statemyPromise.state and resultmyPromise.result.

The following are the common promises terminologies and their corresponding results:

Promise statePromise resultDescription
PendingundefinedWhen an asynchronous function linked to a promise has neither failed nor succeeded.
Fulfilleda result valueThe asynchronous function has succeeded.
Rejectedan error objectThe asynchronous function has failed

Async/Await

Async/await is a modern JavaScript feature that simplifies the handling of asynchronous code.

An asynchronous function is declared using the async keyword. Then the await keyword is used inside the async function to pause its execution until the operation is complete.

Async/await is often used together with promises to handle asynchronous operations more effectively.

When an asynchronous function is invoked, it returns a promise, allowing you to chain multiple async operations together.

async function getData() {
    try {
        // Asynchronous operation using await
        const response = await fetch('https://api.example.com/data');

        // Check if the response was successful
        if (!response.ok) {
            throw new Error('Failed to fetch data');
        }

        // Convert the response to JSON
        const data = await response.json();

        // Process the data
        console.log(data);
    } catch (error) {
        console.error('Error:', error.message);
    }
}

Conclusion

In conclusion, mastering asynchronous JavaScript as a developer is crucial for building modern, responsive web applications.

Additionally, understanding when to employ each asynchronous pattern can significantly enhance the efficiency and maintainability of your codebase.

Further reading and resources