Asynchronous JavaScript (Part 1)

Nikhil ChauhanNikhil Chauhan
10 min read

JavaScript, as a programming language, is known for its simplicity and versatility. However, one of its most powerful and sometimes perplexing features is its ability to handle asynchronous operations. In this blog, we will delve into the world of asynchronous JavaScript, demystifying its concepts and showcasing its capabilities.

How does JS handle Async operations?

  • JS is a single-threaded language.

  • JS by default only supports synchronous code execution, meaning it executes code line by line, waiting for each operation to complete before moving on to the next.

  • NOTE: the above property of synchronous code execution only works for operations natively known to JavaScript.

The above property does not apply to setTimeout and setInterval, lets see this with an example.

console.log("Start");
for (let i = 0; i < 1000000000; i++) {
  //some task
}
console.log("Task Done");
console.log("End");

//output
Start
Task Done
End
console.log("Start");
setTimeout(function exec() {
  console.log("Task Done");
}, 7000);
console.log("End");

//output
Start
End
Task Done

Given that setTimeout and setInterval are not native features of JavaScript, how are they executed? Moreover, if they are not part of JavaScript, how are we still able to use them in our JS code?

We'll understand the later part first.

setTimeout and setInterval are part of the Web APIs provided by the runtime environment in which JavaScript runs, such as web browsers or Node.js. These environments extend the core JavaScript functionality with additional features, including the ability to handle asynchronous operations. This means that when you use setTimeout or setInterval, you are leveraging the capabilities provided by the runtime environment, not JavaScript itself.

Now we know that runtime also provides functionalities that can be leveraged by JS. But how JS handles them?

When we run this code, we have a call stack, an event queue, and an event loop at play:

function process() {
  console.log("Start");
  setTimeout(function exec() {
    console.log("Executed some task");
  }, 3000);
  for (let i = 0; i < 10000000000; i++) {
    //some task
  }
  console.log("End");
}

process();

What Happens in This Code:

  1. Call toprocess Function:

    • The process function is called, and "Start" is immediately logged to the console.
  2. EncounteringsetTimeout:

    • When setTimeout is encountered, JavaScript delegates this task to the runtime environment (like the browser or Node.js) because setTimeout is not a part of the core JavaScript language.

    • The runtime environment starts the 3-second timer and JavaScript continues to the next line without waiting for the timer.

  3. Executing thefor Loop:

    • JavaScript then begins executing the for loop, which is computationally intensive and takes approximately 9 seconds to complete.
  4. Timer Completion:

    • After 3 seconds, the timer completes, and the runtime environment places the exec function into the event queue since the call stack is still busy with the for loop.
  5. Call Stack and Event Loop:

    • JavaScript, being single-threaded and synchronous, continues executing the for loop without interruption, ignoring the event queue until the call stack is clear.

    • The event loop constantly checks if the call stack is empty. It will only push tasks from the event queue to the call stack when the call stack is empty and the global code has finished executing.

  6. End offor Loop and Logging "End":

    • Once the for loop completes after 9 seconds, "End" is logged to the console, and the call stack is finally empty.
  7. Execution ofexec Function:

    • With the call stack empty, the event loop picks the exec function from the event queue and pushes it onto the call stack.

    • The exec function is then executed, logging "Executed some task" to the console.

Callbacks: The Legacy Approach

Using callbacks was the traditional way to handle asynchronous operations in JavaScript. However, callbacks have their own set of challenges.

function fetchCustom(url, fn) {
  console.log("Start Download for", url);
  setTimeout(function process() {
    console.log("Download Completed.");
    let response = "Dummy data";
    fn(response);
    console.log("Ending the function");
  }, 3000);
}

function writeFile(data, fn) {
  console.log("Started writing data", data);
  setTimeout(function process() {
    console.log("Writing completed");
    let filename = "output.txt";
    fn(filename);
    console.log("writing ended");
  }, 4000);
}

function uploadFile(filename, newurl, fn) {
  console.log("Upload started");
  setTimeout(function process() {
    console.log("File", filename, "uploaded successfully on", newurl);
    let uploadResponse = "SUCCESS";
    fn(uploadResponse);
    console.log("upload ended");
  }, 2000);
}

fetchCustom("www.google.com", function downloadCallback(response) {
  console.log("Downloading response is", response);

  writeFile(response, function writeCallback(filenameResponse) {
    console.log("new file written is", filenameResponse);
    uploadFile(
      filenameResponse,
      "www.drive.google.com",
      function uploadCallback(uploadResponse) {
        console.log("Successfully uploaded", uploadResponse);
      }
    );
  });
});

Problems with Callbacks:

  1. Callback Hell: Callbacks within callbacks create a pyramid structure, making code hard to read and maintain.

  2. Inversion of Control: When passing a callback, you give control of its execution to another function, making it difficult to predict and manage.

fn(response); Why Use a Callback Function Here?

When dealing with asynchronous operations like downloading data, we can't simply return the result as we would in synchronous code. This is because the operation takes some time to complete, and the function might return before the operation finishes.

  • fn is the callback function passed as an argument to fetchCustom.

  • We call fn with the response as its argument.

  • This is crucial because fetchCustom can't directly return the response from the asynchronous operation. Instead, it passes the response to the callback function, which can then handle the data.

Promises: The Modern Approach

Promises in JavaScript are special objects that enhance code readability and handle asynchronous operations more efficiently. They immediately return from a function, acting as a placeholder for data we expect to receive from a future task.

Key Concepts of Promises:

What Do Promises Do?

  1. Inside JavaScript:

    • Promises help manage asynchronous code by acting as a placeholder for a value that is not yet available.
  2. Outside JavaScript:

    • Promises sign-up processes to run in the runtime environment and give JavaScript a placeholder with a value property.

Why Use Promises?

  • Readability: Promises make asynchronous code easier to read and understand.

  • Control: They provide better control over asynchronous tasks compared to callbacks, reducing callback hell and inversion of control issues.

How Promises Work Behind the Scenes

Properties of a Promise Object

  1. Status:

    • pending: The initial state, neither fulfilled nor rejected.

    • fulfilled: The operation was completed successfully.

    • rejected: The operation failed.

  2. Value:

    • When the promise is pending, the value is undefined.

    • When fulfilled, the value is updated to the resolved value.

    • When rejected, the value is an error message.

  3. Fulfillment:

    • This is an array of functions attached using .then() that are executed once the promise is fulfilled.

Promise Lifecycle

  1. Pending: The promise is created and starts in a pending state.

  2. Fulfilled: Once the asynchronous operation is complete successfully, resolve is called, updating the promise status to fulfilled and setting the value.

  3. Rejected: If the operation fails, reject is called, updating the status to rejected and setting the error value.

Creating a Promise

A promise is created using the Promise constructor, which takes a callback function with two parameters: resolve and reject. Here's how you create a promise:

function fetch(url) {
  return new Promise(function (resolve, reject) {
    console.log("Start fetching from", url);
    setTimeout(function process() {
      let data = "Dummy data";
      console.log("Completed fetching the data");
      resolve(data); // Call resolve with the data when the task is successful
    }, 4000);
  });
}

If you want to return something on success, then call the resolve() function with whatever value you want to return.

When do we consider a promise fulfilled or rejected?

If we call the resolve() function, we consider it fulfilled. If we call the reject() function, we consider it rejected.

function demo2(val) {
  return new Promise(function (resolve, reject) {
    console.log("Start");
    setTimeout(function process() {
      console.log("Completed timer");
      if ((val & 2) === 0) {
        resolve("EVEN");
      } else {
        reject("ODD");
      }
    }, 10000);
    console.log("Somewhere");
  });
}

let a = demo2(4);
  1. Promise Creation:

    • The function demo2 is called with the argument 4.

    • Inside demo2, a new promise is created and returned immediately.

    • When the promise is created, the console.log("Start") executes, printing "Start".

    • Next, console.log("Somewhere") executes, printing "Somewhere".

  2. Promise State:

    • At this point, the promise is in the pending state because the setTimeout function has started but not yet completed.

    • The setTimeout function sets a timer for 10 seconds (10000 milliseconds).

  3. Timer Completion:

    • After 10 seconds, the setTimeout callback (process function) executes, printing "Completed timer".

    • The condition (val & 2) === 0 checks if the provided value (val) is even.

    • If val is even (in this case, 4), the promise is resolved with the value "EVEN".

    • If val is odd, the promise is rejected with the value "ODD".

  4. Promise Resolution:

    • Once the timer completes and the condition is checked, the promise state transitions from pending to either fulfilled (if resolve is called) or rejected (if reject is called).

    • In this example, the promise will be fulfilled because 4 is even.

  5. Checking Promise State:

    • Initially, when let a = demo2(4); is executed, a is assigned a promise in the pending state.

    • After 10 seconds, when the timer completes, the promise transitions to the fulfilled state with the value "EVEN".

Consuming a Promise in JavaScript

Consuming a promise is a powerful feature in JavaScript that helps us avoid issues like inversion of control. When we call a function that returns a promise, we receive a promise object that can be stored in a variable.

Understanding Promise Execution

Consider this code snippet:

let response = fetchData('www.google.com');

Question:*Will JavaScript wait for the promise to resolve if it involves asynchronous code?*

Answer: If the creation of a promise involves a synchronous piece of code, JavaScript will wait. If it involves an asynchronous piece of code, JavaScript will not wait.

Synchronous Promise Example

function fetchData(url) {
  return new Promise(function (resolve, reject) {
    console.log("Started downloading from ", url);
    for (let i = 0; i < 1000000000; i++) {} // Long synchronous task
    resolve("dummy data");
  });
}

In this example, the promise involves a long synchronous task (the for loop). JavaScript will wait for this loop to complete before resolving the promise. Once the loop is done, the promise is resolved immediately.

Asynchronous Promise Example

function fetchData(url) {
  return new Promise(function (resolve, reject) {
    console.log("Started downloading from ", url);
    setTimeout(function process() {
      let data = "Dummy data";
      console.log("Download complete");
      resolve(data);
    }, 7000);
  });
}

In this example, the promise creation involves an asynchronous task (setTimeout). The promise object is created and returned immediately, with an initial state of pending. The promise is fulfilled after 7 seconds.

Executing Functions When a Promise is Resolved

To execute functions when a promise is resolved, we use the .then() method. This method attaches a callback function that executes once the promise is fulfilled. The .then() method itself returns a new promise.

function fetchData(url) {
  return new Promise(function (resolve, reject) {
    console.log("Started downloading from ", url);
    setTimeout(function process() {
      let data = "Dummy data";
      console.log("Download complete");
      resolve(data);
    }, 7000);
  });
}

let downloadPromise = fetchData("www.google.com");
downloadPromise.then(function handleResolve(value) {
  console.log(value); // Logs "Dummy data"
  return "Hi There!";
});

Key Points

  1. Promise Creation and State:

    • Promises are special JavaScript objects that serve as placeholders for data from future tasks.

    • They transition through states: pending, fulfilled, and rejected.

  2. Synchronous vs Asynchronous Execution:

    • If a promise creation involves synchronous code, JavaScript will wait.

    • For asynchronous tasks, the promise object is returned immediately in a pending state.

  3. Using.then() Method:

    • The .then() method attaches a callback function to be executed when the promise is fulfilled.

    • This method returns a new promise, allowing for chaining multiple asynchronous operations.

Understanding promises is a significant step towards mastering asynchronous programming in JavaScript. Promises offer a cleaner and more manageable way to handle asynchronous operations compared to traditional callbacks, reducing issues like callback hell and inversion of control. By using promises, we can write more readable and maintainable code that handles asynchronous tasks efficiently.

In this blog, we explored how promises work, how to create and consume them, and how they help manage asynchronous operations. In the next part of this series, we will delve deeper into other asynchronous patterns in JavaScript, including async/await, and much more.

Stay tuned for Part 2, where we'll continue our journey into the world of asynchronous JavaScript!

0
Subscribe to my newsletter

Read articles from Nikhil Chauhan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nikhil Chauhan
Nikhil Chauhan