All bout "asynchronous" in JavaScript

·

4 min read

Implementing asynchronous operations is almost inevitable in JavaScript programming if you want optimization.

Today, I will explain about "asynchronous" in JavaScript in 5 minutes.

First, we have to be familiar with the "thread" which is the main concept for asynchronous programming.

What is thread?

A thread means a sequential unit of program execution.

Key features of a thread

  • Programs in a thread shares the same memory space.

  • It is in a thread where programs are executed.

  • A thread can run concurrently with other threads.

  • There are two kinds of execution in thread: single-thread and multi-thread.

Single-thread vs multi-thread

In program runtime, there are two types of approach with thread. One is called single-thread, the other is multi-thread.

Single-thread

  • Only one thread exists in runtime.

  • Programs are run one after another sequentially.

  • If an operation blocks a thread, it cause the entire program to wait.

Multi-thread

  • Multiple threads exists in runtime.

  • Tasks can be executed simultaneously. If there are three threads, three tasks can be run concurrently.

  • In general, it can better utilize computer resources. Therefore, it can help optimization especially in time-consuming tasks.

Thread handling in JavaScript

Thanks to some technologies, JavaScript in both client-side (that is, browser) and server-side (Node.js) support multi-thread, though, the both JavaScript is primarily based on single-thread.

Then, what if we have to handle time-consuming operation? Do we have to wait such an operation to be finished? The answer is no; it is where the asynchronous programing comes in handy.

With the asynchronous programing, multiple progress can be in progress at the same time though they are in fact not executed simultaneously.

Therefore, before time-consuming operations like network requests, file system operation, and database queries have been done next programs can be executed.

Syntax of the asynchronous programing

There are three kinds of asynchronous programing in JavaScript.

  1. Callback

  2. Promise

  3. Async and await

Let's dive deeper into them.

Callback

This is the oldest method of asynchronous programing in JavaScript. Callback is a function passed as an argument in another function, which is invoked after the asynchronous operation completes.

console.log("1: Program starts.");

function fetchData(callbackFunc) {
    console.log("2: Starting asynchronous operation.");
    setTimeout(() => {
        console.log("4: async operation complets.");
        callbackFunc("some data");
    }, 200);
    console.log("3: fetchData initiate async operation not after 2000 ms.");
};

function examleCallback(data) {
    console.log("5: processig data", data);
};

fetchData(examleCallback);

The syntax is somewhat verbose and bit difficult to understand.

The promise method may provide bit easier way.

Promise

Promise represents a value that may not be available immediately but will be resolved in the future.

Promise can be in one of the three states:

  1. Pending: initial state, neither resolved nor rejected.

  2. Fulfileed: the operation completed successfully.

  3. Rejected: the operation failed.

const examplePromise = new Promise((resolve, reject) => {
    const success = true;
    if (success) {
        resolve("Operation succeeded.");
    } else {
        reject("Operation failed.");
    }
});

examplePromise
    .then((result) => {
        console.log(result); // OUTPUT: Operation succeeded.
    })
    .catch((error) => {
        console.error(error); 
    });

Note

  • then handles the fulfillment of the promise.

  • catch handles the rejection of the promise.

  • finally executes regardless of whether the promise is fulfilled or rejected.

Promise is also bit difficult but it is useful in terms of chaining promises.

fetchUserData(userId)
    .then(user => fetchUserPost(user))
    .then(post => fetchPostComments(post))
    .catch(error => console.error(error));

It is important to note Promise.all().
This method takes an array of promises and returns a new promise that fulfills when all of the promises in the array fulfilled, or or rejects if at least one of them rejected.

Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)])
  .then(users => console.log(users))
  .catch(error => console.error(error));

There is another way of asynchronous programing: "async-await".

Async and await

It is an asynchronous programing in a simpler and cleaner way. Therefore, in many cases, it is preferred rather than the ways mentioned.

// some time consuming operation
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve("Data fetched"), 2000);
  });
}

async function getData() {
    const result = await fetchData();
    console.log(result);
}

getData();

In this way, you can achieve asynchronous programing like synchronous way.

Finally, let's compare the three methods with a same result.

// time-consuming function
function simulateAsyncOperation(value, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(value);
    }, delay);
  });
}

// 1. Callback Approach
function performOperationsCallback(callback) {
  console.log("Starting operations (Callback)");
  simulateAsyncOperation("A", 1000).then(resultA => {
    console.log("Result A:", resultA);
    simulateAsyncOperation("B", 500).then(resultB => {
      console.log("Result B:", resultB);
      simulateAsyncOperation("C", 1500).then(resultC => {
        console.log("Result C:", resultC);
        callback("Operations completed (Callback)");
      });
    });
  });
}

// Usage of callback approach
performOperationsCallback((result) => {
  console.log(result);
});

// 2. Promise Approach
function performOperationsPromise() {
  console.log("Starting operations (Promise)");
  return simulateAsyncOperation("A", 1000)
    .then(resultA => {
      console.log("Result A:", resultA);
      return simulateAsyncOperation("B", 500);
    })
    .then(resultB => {
      console.log("Result B:", resultB);
      return simulateAsyncOperation("C", 1500);
    })
    .then(resultC => {
      console.log("Result C:", resultC);
      return "Operations completed (Promise)";
    });
}

// Usage of promise approach
performOperationsPromise().then(result => {
  console.log(result);
});

// 3. Async/Await Approach
async function performOperationsAsync() {
  console.log("Starting operations (Async/Await)");
  const resultA = await simulateAsyncOperation("A", 1000);
  console.log("Result A:", resultA);
  const resultB = await simulateAsyncOperation("B", 500);
  console.log("Result B:", resultB);
  const resultC = await simulateAsyncOperation("C", 1500);
  console.log("Result C:", resultC);
  return "Operations completed (Async/Await)";
}

// Usage of async/await approach
(async () => {
  const result = await performOperationsAsync();
  console.log(result);
})();