Obafemi Emmanuel

JavaScript Asynchronous Programming

Published 3 months ago

JavaScript is a single-threaded language, meaning it executes code sequentially, one operation at a time. This can cause problems when dealing with time-consuming tasks like API requests, file reading, or database queries. To handle such tasks efficiently, JavaScript provides asynchronous programming techniques.

In this guide, we will explore the following key topics:

  1. Synchronous vs Asynchronous Code
  2. Callbacks
  3. Promises (.then, .catch, .finally)
  4. async/await

1. Synchronous vs Asynchronous Code

Synchronous Code

In synchronous programming, each task must complete before the next one starts. This can lead to blocking operations where the entire program waits for a slow task to finish.


Example:

console.log("Start");
for (let i = 0; i < 1000000000; i++) {} // Simulating a time-consuming task
console.log("End");

In this example, JavaScript will block execution until the loop finishes before logging "End".


Asynchronous Code

Asynchronous programming allows tasks to run independently without blocking the execution of other tasks. This is achieved using mechanisms like callbacks, promises, and async/await.

Example:

console.log("Start");
setTimeout(() => {
    console.log("Async Task Complete");
}, 2000);
console.log("End");

Here, JavaScript does not wait for setTimeout to finish. Instead, it continues executing the next statement while setTimeout runs in the background.


2. Callbacks

A callback is a function passed as an argument to another function, which gets executed after the completion of an asynchronous operation.


Example:

function fetchData(callback) {
    setTimeout(() => {
        console.log("Data fetched");
        callback();
    }, 2000);
}

function processData() {
    console.log("Processing data...");
}

fetchData(processData);

Callback Hell

Callbacks can become deeply nested, leading to difficult-to-read and maintain code, known as "callback hell".


Example of Callback Hell:

fetchData(() => {
    processData(() => {
        saveData(() => {
            console.log("All tasks completed");
        });
    });
});

To avoid this, Promises were introduced.


3. Promises

A Promise represents a value that might be available now, in the future, or never. It helps handle asynchronous operations more cleanly.


Promise States

  1. Pending - Initial state, before completion or rejection.
  2. Fulfilled - The operation completed successfully.
  3. Rejected - The operation failed.

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        let success = true;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Error fetching data");
        }
    }, 2000);
});

Using .then(), .catch(), and .finally()

myPromise
    .then((message) => {
        console.log(message);
    })
    .catch((error) => {
        console.error(error);
    })
    .finally(() => {
        console.log("Operation complete");
    });

Chaining Promises

fetchData()
    .then(processData)
    .then(saveData)
    .then(() => console.log("All tasks completed"))
    .catch(error => console.error("Error:", error));

This avoids callback hell and makes the code more readable.


4. async/await

async/await is a modern way to handle asynchronous operations, making the code more synchronous and readable.


Using async/await

async function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Data fetched");
        }, 2000);
    });
}

async function process() {
    console.log("Fetching data...");
    const data = await fetchData();
    console.log(data);
    console.log("Processing complete");
}

process();

Error Handling with try/catch

async function fetchDataWithError() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("Error fetching data");
        }, 2000);
    });
}

async function processWithError() {
    try {
        const data = await fetchDataWithError();
        console.log(data);
    } catch (error) {
        console.error("Caught an error:", error);
    }
}

processWithError();

Conclusion

Asynchronous programming is crucial for handling time-consuming operations efficiently in JavaScript. Here’s a quick recap:

  • Callbacks are simple but can lead to callback hell.
  • Promises provide a cleaner, more manageable approach.
  • async/await makes asynchronous code look synchronous, improving readability and maintainability.

Understanding these concepts will help you write more efficient and readable JavaScript code. Happy coding!


Leave a Comment


Choose Colour