A Simple Guide to Async JavaScript , Web APIs, and Promises

Responding quickly is essential in the fast-paced world of web development. Consumers anticipate fast and seamless loading times from web applications, which is where asynchronous JavaScript, or Async JS, comes into play. In this blog article, we will discuss Async JS, its importance in contemporary web development, and the several ways to implement asynchronous code.

Understanding Asynchronous JavaScript

JavaScript is typically executed synchronously, which means that each operation is finished before going on to the next. However, this synchronous execution may result in performance bottlenecks in a world where web pages are becoming increasingly complex. By enabling non-blocking behavior and enhancing the overall user experience, asynchronous programming enables specific activities to be carried out independently.

Ways to Achieve Asynchronous Execution

  1. Callbacks: The simplest method for achieving asynchronous behavior in JavaScript is to use callbacks. This method involves passing a function as an input to another function, which then executes the function when the operation is finished. Nestled callbacks are useful, but they can cause callback hell, which makes the code difficult to comprehend and update.

  2. Promises: A promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

  3. Async/Await: Async/Await is a syntax built on top of promises that further simplifies asynchronous code. The async keyword is used to define a function that returns a promise, and the await keyword is used to wait for the promise to resolve.

Before diving further into Promises, we will discuss Web APIs.

Web APIs

Asynchronous actions in JavaScript are mostly handled by web APIs, or application programming interfaces. The browser environment provides web APIs, which let the browser communicate with different parts of the online platform. Facilitating asynchronous programming is one of the main uses for Web APIs, and promises and other asynchronous patterns are frequently combined with them.

  • Event Loop: The event loop is a fundamental concept in asynchronous JavaScript. It continuously checks the message queue for new tasks and executes them in a non-blocking way.

  • Timers: Web APIs include timers such as setTimeout and setInterval. These timers allow you to schedule code execution after a specified delay or at regular intervals. When the timer expires, the associated callback function is pushed into the message queue, and the event loop picks it up for execution.

  • Promises: While promises themselves are part of the JavaScript language, Web APIs often interact with them. For example, the fetch API returns a promise that resolves with the response to a request.

Promises and their functions

Promises are a feature in JavaScript that provides a way to deal with asynchronous operations. They help manage and organize asynchronous code, making it more readable and maintainable. A promise represents the eventual completion or failure of an asynchronous operation and its resulting value.

A promise can be created with the constructor syntax, like this:

let promise = new Promise(function(resolve, reject) {
  // Code to execute
});

The constructor function takes a function as an argument. This function is called the executor function.

// Executor function passed to the 
// Promise constructor as an argument
function(resolve, reject) {
    // Your logic goes here...
}

The executor function takes two arguments: resolve and reject. These are the callbacks provided by the JavaScript language. Your logic goes inside the executor function that runs automatically when a new promise is created.

The new Promise() constructor returns a promise object. As the executor function needs to handle async operations, the returned promise object should be capable of informing the user when the execution has been started, completed (resolved), or returned with an error (rejected).

States of promise:

  • Pending: The initial state. The promise is neither fulfilled nor rejected.

  • Fulfilled: The asynchronous operation is completed successfully, and the promise has a resulting value.

  • Rejected: The asynchronous operation encountered an error or failed, and the promise has a reason for the failure.

This Promises code structure represents a common pattern in asynchronous JavaScript, where promises are used to handle asynchronous tasks and provide a clean and organized way to handle both success and error scenarios.

// Function that returns a promise to simulate fetching data from an API
function fetchData() {
  return new Promise((resolve, reject) => {
    // Simulate an API request with a delay
    setTimeout(() => {
      const success = Math.random() > 0.5; // Simulating success or failure randomly

      if (success) {
        const data = { message: "Data fetched successfully!" };
        resolve(data);
      } else {
        reject(new Error("Failed to fetch data"));
      }
    }, 2000); // Simulating a 2-second delay
  });
}

// Using the promise
fetchData()
  .then((result) => {
    console.log(result.message);
    // Perform additional tasks with the fetched data
  })
  .catch((error) => {
    console.error(error.message);
    // Handle errors
  });

"then" and "catch" are methods that you can use to handle the results of a promise: the fulfilled state and the rejected state, respectively.

Some of the promise-based functions are as follows:

  • Promise.resolve()

    It returns a resolved promise with a specified value. It is often used when you want to create a promise that is resolved immediately.

      const resolvedPromise = Promise.resolve("Resolved value");
    
      resolvedPromise.then((value) => {
        console.log(value); // Output: Resolved value
      });
    
  • Promise.reject()

    It returns a rejected promise with a specified reason (usually an error). It is used when you want to create a promise that is rejected immediately.

      const rejectedPromise = Promise.reject(new Error("Something went wrong"));
    
      rejectedPromise.catch((error) => {
        console.error(error.message); // Output: Something went wrong
      });
    
  • Promise.all()

    It returns a promise that fulfills with an array of values when all promises in the iterable are fulfilled, or rejects with the reason of the first promise that rejects.

      const promise1 = Promise.resolve("One");
      const promise2 = "Two"; // Resolved value
      const promise3 = new Promise((resolve) => setTimeout(() => resolve("Three"), 1000));
    
      Promise.all([promise1, promise2, promise3])
        .then((values) => {
          console.log(values); // Output: ['One', 'Two', 'Three']
        })
        .catch((error) => {
          console.error(error);
        });