Asynchronous Javascript

Introduction

Asynchronous operations are tasks that can be executed independently of the main program flow, allowing the program to continue executing other tasks while waiting for the asynchronous operation to complete.

How JavaScript Executes Code

Single-Threaded: Unlike some other languages like Java or Python, JavaScript runs on a single thread. This means it can only execute one piece of code at a time.

Call stack: The call stack is a data structure used by JavaScript to manage the execution of function calls. When you execute a script, JavaScript starts with a global execution context and an empty call stack.

Example:

function hello() {
  console.log("Hello");
}

function welcome() {
  console.log("Welcome");
}

function main() {
  hello();
  welcome();
}

main();
  1. main() is called, adding it to the call stack.

  2. Inside main(), hello() is called, adding it to the call stack above main().

  3. hello() logs "Hello" and completes, so it is removed from the call stack.

  4. Control returns to main(), which calls welcome(), adding it to the call stack above main().

  5. welcome() logs "Welcome" and completes, so it is removed from the call stack.

  6. Control returns to main(), which completes, so it is removed from the call stack.

  7. The call stack is now empty, and the script has finished executing.

Sync vs. Async

In synchronous code, each operation is executed one after the other, in sequence. This can lead to blocking behavior, especially when dealing with time-consuming tasks like network requests or file I/O.

Asynchronous code, on the other hand, allows multiple operations to be performed simultaneously, improving performance and responsiveness.

Example:-

// Synchronous function
function syncFunction() {
  console.log("Synchronous function");
}

console.log("Before calling syncFunction");

// Calling the synchronous function
syncFunction();

console.log("After calling syncFunction");
// Asynchronous function
function asyncFunction() {
  setTimeout(() => {
    console.log("Asynchronous function");
  }, 2000); // Simulating a delay of 2 seconds
}

console.log("Before calling asyncFunction");

// Calling the asynchronous function
asyncFunction();

console.log("After calling asyncFunction");

Converting Sync to Async

To convert synchronous code to asynchronous, we utilize callbacks, promises, or async/await.

Callbacks and Their Drawbacks

Callbacks are functions passed as arguments to other functions, to be executed later once a particular task is completed. While effective, they can lead to callback hell, making code hard to read and maintain.

Promises: Solving the Inversion of Control

Promises were introduced to address the drawbacks of callbacks, providing a cleaner and more readable way to handle asynchronous operations. They represent a value that may be available now, or in the future, or never. Promises allow chaining multiple asynchronous operations and provide error handling through the then() and catch() methods.

Event Loop

The event loop is JavaScript's mechanism for handling asynchronous operations. It continuously checks the call stack and the task queue, moving tasks from the queue to the stack when the stack is empty.

Different Functions In Promises

Here's a brief explanation of each of these Promise methods in JavaScript:

  1. Promise.resolve(value):

    • Returns a Promise object that is resolved with the given value. If the value is a promise, that promise is returned unchanged. If the value is a thenable (i.e., an object with a then method), the returned promise will "follow" that thenable, adopting its eventual state.

    • Example:

    •     const resolvedPromise = Promise.resolve(42);
          resolvedPromise.then(value => console.log(value));
      
  2. Promise.reject(reason):

    • Returns a Promise object that is rejected with the given reason.

    • Example:

        const rejectedPromise = Promise.reject(new Error('Error occurred'));
        rejectedPromise.catch(error => console.error(error.message));
      
  3. Promise.all(iterable):

    • Returns a single Promise that resolves when all of the promises in the iterable argument have resolved, or rejects with the reason of the first promise that rejects.

    • Example:

        const promise1 = Promise.resolve(1);
        const promise2 = Promise.resolve(2);
        const promise3 = Promise.resolve(3);
      
        Promise.all([promise1, promise2, promise3])
          .then(values => console.log(values));
      
  4. Promise.allSettled(iterable):

    • Returns a promise that resolves after all of the given promises have either resolved or rejected, with an array of objects that each describe the outcome of each promise.

    • Example:

        const promise1 = Promise.resolve(1);
        const promise2 = Promise.reject(new Error('Rejected'));
        const promise3 = Promise.resolve(3);
      
        Promise.allSettled([promise1, promise2, promise3])
          .then(results => console.log(results));
        // Output: [{status: "fulfilled", value: 1}, {status: "rejected", reason: Error: Rejected}, {status: "fulfilled", value: 3}]
      
  5. Promise.any(iterable):

    • Returns a single promise that resolves as soon as one of the promises in the iterable resolves. If all of the promises are rejected, it returns a rejected promise with an AggregateError.

    • Example:

        const promise1 = new Promise((resolve, reject) => setTimeout(reject, 100, 'Error 1'));
        const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 200, 'Result 2'));
        const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 300, 'Result 3'));
      
        Promise.any([promise1, promise2, promise3])
          .then(value => console.log(value)); // Output: Result 2
      
  6. Promise.race(iterable):

    • Returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

    • Example:

        const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'Result 1'));
        const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 200, 'Result 2'));
      
        Promise.race([promise1, promise2])
          .then(value => console.log(value));
      

Conclusion

Understanding asynchronous JavaScript is crucial for building fast and effective apps. With tools like promises and the event loop, developers can write code that handles many tasks at once without slowing down. Asynchronous programming is key to creating modern web experiences that work smoothly and efficiently.