Understanding Asynchronous Code in JavaScript: A Comprehensive Guide

Understanding Asynchronous Code in JavaScript: A Comprehensive Guide

How JS execute the code?

JavaScript is a synchronous (Moves to the next line only when the execution of the current line is completed) and single-threaded (Executes one command at a time in a specific order one after another serially) language. To know behind the scenes of how JavaScript code gets executed internally, we have to know something called Execution Context and its role in the execution of JavaScript code.

Context: Everything in JavaScript is wrapped inside the Execution Context, which is an abstract concept (can be treated as a container) that holds the information about the environment within which the current JavaScript code is being executed.Now, an Execution Context has two components and JavaScript code gets executed in two phases.

Memory Allocation Phase: In this phase, all the functions and variables of the JavaScript code get stored as a key-value pair inside the memory component of the execution context. In the case of a function, JavaScript copies the whole function into the memory block but in the case of variables, it assigns undefined as a placeholder.

Code Execution Phase: In this phase, the JavaScript code is executed one line at a time inside the Code Component (also known as the Thread of execution) of the Execution Context.

What is the difference between synchronous and asynchronous code in javascript?

In JavaScript, the difference between synchronous and asynchronous code lies in how the code is executed:
Synchronous code:

  • Executes sequentially: Each line of code is executed one after the other, in the order it appears.

  • Blocks the execution: If a line of code takes a long time to execute, the entire program waits for it to finish before moving on. This can lead to unresponsive interfaces.

  • Example:

console.log("First line");
console.log("Second line"); // This line waits for the first line to finish

Asynchronous code:

  • Executes independently: Tasks can start, run, and complete independently of each other.

  • Non-blocking: Asynchronous code allows the program to continue executing other tasks while waiting for long-running operations to finish, such as fetching data from a server.

  • Uses callbacks, promises, or async/await: To handle asynchronous operations, JavaScript uses mechanisms like callbacks, promises, or the async/await syntax.

  •   console.log("First line");
    
      fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error(error));
    
      console.log("Second line"); // This line doesn't wait for the fetch operation to finish
    

Ways to convert into Async code?

JavaScript offers a few ways to handle asynchronous code, each building upon the previous:

1. Callbacks:

The traditional way to handle asynchronous operations is through callbacks. A callback is a function passed as an argument to another function and is executed after the asynchronous operation completes.

function fetchData(callback) {
  setTimeout(() => {
    const data = "Some data";
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data); // Output: "Some data"
});

2. Promises:

Promises provide a cleaner and more flexible way to handle asynchronous operations. A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Some data";
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data); // Output: "Some data"
  })
  .catch((error) => {
    console.error(error);
  });

3. Async/Await:

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // Expected output: "resolved"
}

asyncCall();

What is the callstack & event loop?

Stack: This is where all your javascript code gets pushed and executed one by one as the interpreter reads your program, and gets popped out once the execution is done. If your statement is asynchronous: setTimeout, ajax(), promise, or click event, then that code gets forwarded to Event table, this table is responsible for moving your asynchronous code to callback/event queue after specified time.

Heap: This is where all the memory allocation happens for your variables, that you have defined in your program.

Callback Queue: This is where your asynchronous code gets pushed to, and waits for the execution.

Event Loop: Then comes the Event Loop, which keeps running continuously and checks the Main stack, if it has any frames to execute, if not then it checks Callback queue, if Callback queue has codes to execute then it pops the message from it to the Main Stack for the execution.

Job Queue: Apart from Callback Queue, browsers have introduced one more queue which is “Job Queue”, reserved only for new Promise() functionality. So when you use promises in your code, you add .then() method, which is a callback method. These thenable methods are added to Job Queue once the promise has returned/resolved, and then gets executed.

Quick Question now: Check these statements for example, can you predict the sequence of output?:

console.log('Message no. 1: Sync');
setTimeout(function() {
   console.log('Message no. 2: setTimeout');
}, 0);
var promise = new Promise(function(resolve, reject) {
   resolve();
});
promise.then(function(resolve) {
   console.log('Message no. 3: 1st Promise');
})
.then(function(resolve) {
   console.log('Message no. 4: 2nd Promise');
});
console.log('Message no. 5: Sync');

Some of you might answer this:

// Message no. 1: Sync
// Message no. 5: Sync
// Message no. 2: setTimeout
// Message no. 3: 1st Promise
// Message no. 4: 2nd Promise

because setTimeout was pushed to Callback Queue first, then promise was pushed. But this is not the case, the output will be:

// Message no. 1: Sync
// Message no. 5: Sync
// Message no. 3: 1st Promise
// Message no. 4: 2nd Promise
// Message no. 2: setTimeout

All thenable callbacks of the promise are called first, then the setTimeout callback is called.

What is Async and how callback will be useful for it?

Asynchronous programming allows a program to start a long-running task without blocking other events while the task runs. A callback is a function passed into another function, to be called when the task is finished. Callbacks are often used for asynchronous events like file I/O or network requests.

Explanation

  • Asynchronous programming

    Enables the program to be responsive to other events while a potentially long-running task is running.

  • Callback

    A function passed to another function, to be called when the task is finished. The callback function is executed in the background and calls the callback function when it's done.

Benefits of asynchronous programming

  • Improves the speed of a program by allowing multiple processes to run at the same time.

  • The program can still be responsive to other events while a long-running task runs.

Drawbacks of callbacks Can become hard to read or maintain and Can lead to "callback hell".

Alternatives to callbacks Promises and Async/Await.

What is an inversion of control and callback hell?

Inversion of control and callback hell are both concepts in JavaScript that can affect the readability and maintainability of code:

  • Inversion of control

    In this concept, you give control of your code to a third party, like a library or API, in one part of a program, but then hand control back to a callback in another part. This can be a problem with callbacks.

  • Callback hell

    Also known as the "pyramid of doom", this happens when callbacks are nested within each other, forming a pyramid structure. This can make code hard to read and debug.

Some ways to avoid callback hell include using promises and async/await. Promises have three stages: resolved, rejected, and pending, which can make code more understandable and maintainable. Promises can also solve the main problem of callback hell by providing chaining, which can make code cleaner and more readable

What is promise and what problem does promise is solving?

A promise is a result object that is used to handle asynchronous operations in JavaScript. It represents a value that may not be available yet, but will be resolved at some point in the future.

Promises solve the problem of handling asynchronous operations in a more manageable and readable way. Before promises, developers used callbacks to handle asynchronous operations, which led to a phenomenon known as "callback hell".

Callback hell refers to the situation where you have multiple nested callbacks, making the code hard to read, maintain, and debug. It's a problem that arises when you have to handle multiple asynchronous operations that depend on each other.

Here's an example of callback hell:

asyncOperation1(function(result1) {
  asyncOperation2(result1, function(result2) {
    asyncOperation3(result2, function(result3) {
      console.log(result3);
    });
  });
});

Promises solve this problem by providing a way to handle asynchronous operations in a more linear and synchronous-looking way. A promise represents the result of an asynchronous operation, and you can use methods like then() and catch() to handle the result when it's available.

Here's an example of using promises to handle asynchronous operations:

asyncOperation1()
  .then(result1 => asyncOperation2(result1))
  .then(result2 => asyncOperation3(result2))
  .then(result3 => console.log(result3))
  .catch(error => console.error(error));

Promises provide several benefits over callbacks:

  1. Readability: Promises make the code more readable by allowing you to write asynchronous code that looks synchronous.

  2. Error handling: Promises provide a way to handle errors in a centralized way, using the catch() method.

  3. Chaining: Promises allow you to chain multiple asynchronous operations together, making it easier to handle complex workflows.

What are the different stages in promise?

A promise in JavaScript goes through several stages during its lifecycle. Here are the different stages of a promise:

  1. Pending: This is the initial stage of a promise. When a promise is created, it is in the pending stage. At this stage, the promise has not started executing yet, and it's waiting for the asynchronous operation to complete.

  2. Fulfilled: When the asynchronous operation completes successfully, the promise moves to the fulfilled stage. In this stage, the promise has a value, which is the result of the asynchronous operation.

  3. Rejected: If the asynchronous operation fails or throws an error, the promise moves to the rejected stage. In this stage, the promise has a reason, which is the error that occurred during the operation.

  4. Settled: A promise is said to be settled when it's either fulfilled or rejected. At this stage, the promise has a value or a reason, and it's no longer pending.

let promise = new Promise((resolve, reject) => {
  // pending stage
  setTimeout(() => {
    // fulfilled stage
    resolve("Success!");
  }, 2000);
});

promise.then((value) => {
  console.log(value); // Output: Success!
}).catch((error) => {
  console.error(error);
});

How to create a promise? How to consume a promise?

Creating a promise and consuming it involves using JavaScript's Promise API. Promises are used for asynchronous programming, allowing you to handle operations that will complete in the future, such as fetching data from a server or reading a file. Here’s how you can create and consume a promise:

Creating a Promise

To create a promise, you typically create a function that returns a promise object. The promise constructor takes a function as an argument, which has two parameters: resolve and reject.

function fetchData() {
    return new Promise((resolve, reject) => {
        // Simulating an asynchronous operation (e.g., fetching data)
        setTimeout(() => {
            // Assuming data fetching is successful
            let data = { id: 1, name: 'Example' };
            resolve(data); // Resolve with the fetched data
        }, 2000); // Simulating a delay of 2 seconds
    });
}

In this example:

  • fetchData is a function that returns a promise.

  • Inside the promise constructor, resolve is called when the operation is successful and reject is called when it fails.

Consuming a Promise

To consume (or use) a promise, you typically use .then() and .catch() methods. These methods allow you to handle the resolved value or any errors that occur during the asynchronous operation.

Here’s how you can consume the fetchData promise:

fetchData()
    .then(data => {
        console.log('Data received:', data);
        // You can do further processing with the data here
    })
    .catch(error => {
        console.error('Error fetching data:', error);
        // Handle any errors that occurred during fetching
    });

In this example:

  • .then(data => { ... }) handles the resolved data. data represents the value passed to resolve inside the promise constructor (fetchData function).

  • .catch(error => { ... }) handles any errors that occur during the promise execution. error represents the value passed to reject inside the promise constructor.

Chaining of promise using .then?

You can also chain promises using .then() if you have multiple asynchronous operations to perform sequentially:

fetchData()
    .then(data => {
        // Process data
        return processFurther(data); // Assuming processFurther returns a promise
    })
    .then(result => {
        console.log('Further processing result:', result);
    })
    .catch(error => {
        console.error('Error:', error);
    });

In this chain:

  • The first .then() handles the data returned by fetchData.

  • It then returns another promise (processFurther(data)), which is handled by the next .then().

  • Any errors in the chain are caught by the final .catch().

Handling errors in promises

Handling errors in promises is an essential part of working with asynchronous code in JavaScript. Here are some ways to handle errors in promises:

1. Using catch method

The catch method is used to handle errors in promises. It takes a callback function as an argument, which is called when the promise is rejected.

VerifyOpen In EditorEditCopy code1promise
2  .then((value) => {
3    console.log(value);
4  })
5  .catch((error) => {
6    console.error(error);
7  });

In this example, if the promise is rejected, the catch method will be called with the error as an argument.

2. Using try-catch block inside then method

You can also use a try-catch block inside the then method to catch errors.

VerifyOpen In EditorEditCopy code1promise
2  .then((value) => {
3    try {
4      // code that might throw an error
5    } catch (error) {
6      console.error(error);
7    }
8  });

However, this approach has a limitation. If an error is thrown in the then method, it will not be caught by the catch method. Instead, it will be caught by the global error handler.

3. Using Promise.prototype.catch method

The Promise.prototype.catch method is similar to the catch method, but it's a part of the Promise prototype. It allows you to catch errors in a promise chain.

VerifyOpen In EditorEditCopy code1promise
2  .then((value) => {
3    console.log(value);
4  })
5  .then(() => {
6    // another promise
7  })
8  .catch((error) => {
9    console.error(error);
10  });

In this example, if any of the promises in the chain are rejected, the catch method will be called with the error as an argument.

4. Using async/await syntax

With async/await syntax, you can use try-catch blocks to handle errors in promises.

VerifyOpen In EditorEditCopy code1async function foo() {
2  try {
3    const value = await promise;
4    console.log(value);
5  } catch (error) {
6    console.error(error);
7  }
8}

In this example, if the promise is rejected, the catch block will be executed with the error as an argument.

How different promise based functions works

Promise.resolve(value)

Returns a resolved promise with the specified value.

let promise = Promise.resolve('Success');

promise.then(value => {
  console.log(value); // Output: Success
});

Promise.reject(reason)

Returns a rejected promise with the specified reason.

let promise = Promise.reject('Error');

promise.catch(error => {
  console.error(error); // Output: Error
});

Promise.all(promises) [fail fast]

Resolves when all promises in the array resolve, or rejects if any promise rejects.It will not wait for other promises it will fail quickly and return.

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log(results); // Output: ['Result 1', 'Result 2', 'Result 3']
  })
  .catch(error => {
    console.error(error);
  });

Promise.allSettled(promises)

Resolves when all promises in the array have settled (resolved or rejected), providing an array of results.It will wait for first seeking result and return it . if all of them fails then it will return an aggregate error of arrays.

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    console.log(results);
    // Output: [{ status: 'fulfilled', value: 'Result 1' }, { status: 'rejected', reason: 'Error 2' }, { status: 'fulfilled', value: 'Result 3' }]
  });

Promise.any(promises)

Resolves with the value of the first resolved promise, or rejects if all promises reject.

Promise.any([promise1, promise2, promise3])
  .then(result => {
    console.log(result); // Output: Result 2 (first resolved promise)
  })
  .catch(error => {
    console.error(error);
  });

Promise.race(promises)

Resolves or rejects when the first promise in the array resolves or rejects.if it will be error it will return error and if it will be Settled it will return settled.

Promise.race([promise1, promise2, promise3])
  .then(result => {
    console.log(result); // Output: Result 2 (first to resolve)
  })
  .catch(error => {
    console.error(error);
  });

What is async..await & why do we use it?

What is async/await?

Async/await is a syntax sugar on top of promises that allows you to write asynchronous code that looks and feels like synchronous code. It's a way to write asynchronous code that's easier to read and maintain.

How does it work?

The async keyword is used to define an asynchronous function, which returns a promise. The await keyword is used inside an async function to pause the execution of the function until a promise is resolved or rejected.

Here's an example:

VerifyOpen In EditorEditCopy code1async function fetchData() {
2  const response = await fetch('https://example.com/data');
3  const data = await response.json();
4  console.log(data);
5}

In this example, the fetchData function is marked as async, which means it returns a promise. The await keyword is used to pause the execution of the function until the fetch promise is resolved, and then again until the response.json() promise is resolved.

Why do we use async/await?

We use async/await for several reasons:

  1. Readability: Async/await makes asynchronous code look and feel like synchronous code, which makes it easier to read and understand.

  2. Simplifies error handling: With async/await, you can use try-catch blocks to handle errors in a more traditional way, rather than using .catch() methods.

  3. Easier to write: Async/await makes it easier to write asynchronous code, especially when working with multiple promises.

  4. Improves code organization: Async/await allows you to write asynchronous code that's more modular and organized, rather than having a long chain of .then() methods.

  5. Better support for debugging: With async/await, you can use traditional debugging tools, such as breakpoints and console logs, to debug your asynchronous code.

When to use async/await?

You should use async/await when:

  1. You need to write asynchronous code that's easy to read and maintain.

  2. You need to handle errors in a more traditional way using try-catch blocks.

  3. You're working with multiple promises and need to simplify your code.

  4. You want to improve the organization and readability of your asynchronous code.

In summary, async/await is a syntax sugar on top of promises that makes asynchronous code look and feel like synchronous code. It's easier to read, write, and maintain, and provides better support for error handling and debugging.