# Asynchronous Programming in JavaScript

In the world of JavaScript, understanding asynchronous programming is essential for building responsive and efficient applications. Let's dive deep into these concepts with clear explanations and practical examples.

## How JavaScript Code is Executed

JavaScript is a single-threaded and synchronous programming language. It can perform one task at a time but can manage multiple tasks through asynchronous operations without blocking the main execution thread.

JavaScript can execute code in two ways:  
\- Synchronously  
\- Asynchronously

Now let's dive deep into synchronous and asynchronous execution inside JavaScript.

## Synchronous Execution in JavaScript

JavaScript is synchronous, blocking, and single-threaded in nature. This means that the JavaScript engine executes our program sequentially, one line at a time from top to bottom in the exact order of the statements.

Example:

```javascript
console.log("First line");
console.log("Second line");
console.log("Third line");
```

Output:  
First line  
Second line  
Third line

From the above code, we can see that the code is getting executed sequentially line by line.

## Asynchronous Execution in JavaScript

Unlike synchronous operations, an asynchronous operation does not block the next task from getting executed even if the current task isn’t completed yet. A lot of APIs are being provided to JavaScript by the runtime, which helps JavaScript to behave asynchronously. This asynchronous behavior in JavaScript is achieved through the use of callbacks and promises.  
  
Example:

```javascript
console.log("Start");
setTimeout(function exec() {
  console.log("Timer done");
}, 0);
console.log("End");
```

Output:  
Start  
End  
Timer done  

From the above example, we can see that the \`End\` is getting printed before \`Timer done\`.

JavaScript needs Runtime for its execution. Runtime provides a lot of functionalities to JavaScript. Before diving deep into understanding how JavaScript executes code asynchronously, let's first understand some terminologies that we will use further.

### Execution Context

An execution context is an environment where JavaScript code runs and executes. When the JavaScript engine scans a script file, it makes an environment called the Execution Context.

There are two types of Execution Contexts:  
\- Global Execution Context  
\- Function Execution Context  
  
An Execution Context has two phases:  
\- Memory creation  
\- Code Execution

### Call Stack

The call stack is a part of the JavaScript engine that helps keep track of function calls. When a function gets invoked, it is pushed to the call stack where its execution begins, and when the execution is complete, the function gets popped off the call stack.

### Event Loop

The Event Loop runs indefinitely and it connects the call stack, the microtask queue, and the callback queue. The Event loop keeps on checking whether the call stack is empty and the global piece of code is done. If the call stack gets empty and the global piece of code is also done, then the event loop moves asynchronous tasks from the microtask queue and the callback queue to the call stack for execution.

### Callback Queue

Callback queue stores all the callback functions from setTimeout() before they are moved to the call stack for execution.

### Microtask Queue

Microtask queue stores all the callback functions from Promises before they are moved to the call stack for execution. Now let's understand how the below code will get executed asynchronously by JavaScript.

Example:

```javascript
console.log("Start");
setTimeout(function exec() {
  console.log("Timer done");
}, 0);
console.log("End");
```

#### Execution Order

* First of all, there is \`console.log("Start")\`, so it will log \`Start\` in the console.
    
* After this, there is the \`setTimeout()\` function, which will pass the callback inside the callback queue.
    
* After this, there is \`console.log("End")\`, so it will log \`End\` in the console.
    
* The global piece of code is done and the call stack is also empty.
    
* So, now event loop pushes the callback function from the callback queue inside the call stack.
    
* Now, the JavaScript Engine executes this callback function and logs \`Timer done\` in the console.
    

### Inversion of Control

In simple terms, Inversion of Control (IoC) in JavaScript means giving control of how things happen in your code to an external system or framework, rather than your code directly controlling everything.

Example:

```javascript
function foo(x, callback) {
  for (let i = 0; i < x; i++) {
    console.log(x);
  }

  callback(x * x);
}

foo(10, function executor(num) {
  console.log(num);
});
```

In the above example, let's suppose, `foo` function is written by someone else. foo function takes a callback function as an argument. We are passing `executor` function as a callback to `foo` function. Now, `executor` function is our implementation. So, we are passing the control of calling our callback function to another function foo. This problem is called as the Inversion of control in which the control of calling our callback function is given to some other function.

### Callback Hell

Callback Hell in JavaScript refers to a situation where we have lots of nested callback functions inside each other, which makes code look like a tangled mess. It's like a deep, dark pit where the code gets lost and hard to follow.

Example:

```javascript
const heading1 = document.querySelector(".heading1");
const heading2 = document.querySelector(".heading2");
const heading3 = document.querySelector(".heading3");
const heading4 = document.querySelector(".heading4");
const heading5 = document.querySelector(".heading5");


function changeText(element, text, color, time, onSuccessCallback, onFailureCallback) {
	setTimeout(()=>{
		if(element){
			element.textContent = text;
			element.style.color = color;
			if(onSuccessCallback){
				onSuccessCallback();
			}
		}else{
			if(onFailureCallback){
				onFailureCallback();
			}
		}
	}, time);
}


changeText(heading1, "one", "violet", 1000, ()=>{
	changeText(heading2, "two", "purple", 2000, ()=>{
		changeText(heading3, "three", "red", 1000, ()=>{
			changeText(heading4, "four", "green", 2000, ()=>{
				changeText(heading5, "five", "blue", 1000, ()=>{
					
				}, ()=>{console.log("Heading5 does not exist")});
			}, ()=>{console.log("Heading4 does not exist")});
		}, ()=>{console.log("Heading3 does not exist")});
	}, ()=>{console.log("Heading2 does not exist")});
}, ()=>{console.log("Heading1 does not exist")});
```

### Promises

Promises in JavaScript are objects that represent the eventual completion or failure of an asynchronous operation. They provide a cleaner and more organized way to work with asynchronous code compared to traditional callback functions. A promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

There are three states of a Promise object.

1. Pending
    
2. Fulfilled
    
3. Rejected
    

In case of Promises, we are not passing the control of calling our callback function to some other function. In this case, we are having the control of calling our callback function with ourselves.

#### Creating a Promise

We create a new Promise object by calling the \`new Promise()\` constructor. This constructor takes a function as an argument, commonly referred to as the executor function. Inside this function, we perform our asynchronous operation, and we resolve the promise when it's successful or reject it if an error occurs.

Example:

```javascript
function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

function createPromiseWithTimeout() {
  return new Promise(function executor(resolve, reject) {
    setTimeout(function () {
      let num = getRandomInt(10);
      if (num % 2 == 0) {
        // if the random number is even, we fulfill the Promise
        resolve(num);
      } else {
        // if the random number is odd, we reject the Promise
        reject(num);
      }
    }, 10000);
  });
}

let y = createPromiseWithTimeout();
console.log(y);
```

### Web Browser APIs

(Application Programming Interfaces) are built-in libraries provided by web browsers that allow developers to interact with the browser and its environment.  
These APIs enable web applications to perform various functions, such as manipulating the Document Object Model (DOM), handling user input, making network requests, and storing data locally.

Some key Web Browser APIs:

1. **DOM (Document Object Model) API**
    
    * It manipulates and interacts with HTML and XML documents.
        
    * Example:
        
    * ```javascript
          document.getElementById('example').textContent = 'Hello, World!';
        ```
        
2. **Fetch API**
    
    * It is used for making network requests to send or retrieve data.
        
    * Example:
        
    * ```javascript
          fetch('https://api.example.com/data')
            .then(response => response.json())
            .then(data => console.log(data))
            .catch(error => console.error('Error:', error));
        ```
        
3. **Local Storage API**
    
    * It is used for storing data locally on the user's browser.
        
    * Example:
        
    * ```javascript
          localStorage.setItem('key', 'value');
          const value = localStorage.getItem('key');
          console.log(value);
        ```
        

### Promises Functions

Promises in JavaScript provide a robust way to handle asynchronous operations. They come with several methods that allow you to work with them efficiently.  
Here are the main functions associated with Promises:

1. `Promise.resolve()`
    
    * It creates a Promise that is resolved with a given value.
        
    * Example:
        
    * ```javascript
          const resolvedPromise = Promise.resolve('Resolved value');
          resolvedPromise.then(value => console.log(value)); // Output: Resolved value
        ```
        
2. `Promise.reject()`
    
    * It creates a Promise that is rejected with a given reason.
        
    * Example:
        
    * ```javascript
          const rejectedPromise = Promise.reject('Rejected reason');
          rejectedPromise.catch(reason => console.log(reason)); // Output: Rejected reason
        ```
        
3. `Promise.then()`
    
    * It adds fulfillment and rejection handlers to the Promise and returns a new Promise resolving the return value of the handler.
        
    * Example:
        
    * ```javascript
          const promise = new Promise((resolve, reject) => {
            resolve('Success');
          });
        
          promise.then(value => {
            console.log(value); // Output: Success
          });
        ```
        
4. `Promise.catch()`
    
    * It adds a rejection handler to the Promise and returns a new Promise resolving to the return value of the handler.
        
    * Example:
        
    * ```javascript
          const promise = new Promise((resolve, reject) => {
            reject('Error');
          });
        
          promise.catch(error => {
            console.log(error); // Output: Error
          });
        ```
        
5. `Promise.finally()`
    
    * It adds a handler to be called when the Promise is settled (either fulfilled or rejected). The handler doesn't receive any arguments and returns a Promise.
        
    * Example:
        
    * ```javascript
          const promise = new Promise((resolve, reject) => {
            resolve('Success');
          });
        
          promise.finally(() => {
            console.log('Promise is settled');
          }).then(value => console.log(value)); // Output: Promise is settled followed by Success
        ```
        
6. `Promise.all()`
    
    * It takes an iterable object of Promises as input and it returns a Promise that resolves when all of the Promises in the iterable object have resolved or rejects with the reason of the first promise that rejects.
        
    * Example:
        
    * ```javascript
          const promise1 = Promise.resolve(3);
          const promise2 = 42;
          const promise3 = new Promise((resolve, reject) => {
            setTimeout(resolve, 100, 'foo');
          });
        
          Promise.all([promise1, promise2, promise3]).then(values => {
            console.log(values); // Output: [3, 42, "foo"]
          });
        ```
        
7. `Promise.allSettled()`
    
    * It takes an iterable of Promises and 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:
        
    * ```javascript
          const promise1 = Promise.resolve(3);
          const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'error'));
        
          Promise.allSettled([promise1, promise2]).then(results => {
            results.forEach(result => console.log(result.status));
          });
          // Output: "fulfilled" followed by "rejected"
        ```
        
8. `Promise.race()`
    
    * It takes an iterable of Promises and returns a Promise that resolves or rejects as soon as one of the Promises in the iterable resolves or rejects.
        
    * Example:
        
    * ```javascript
          const promise1 = new Promise((resolve, reject) => {
            setTimeout(resolve, 500, 'one');
          });
        
          const promise2 = new Promise((resolve, reject) => {
            setTimeout(resolve, 100, 'two');
          });
        
          Promise.race([promise1, promise2]).then(value => {
            console.log(value); // Output: "two"
          });
        ```
        

### Error Handling in Promises

Handling errors in Promises is crucial for building robust and reliable JavaScript applications. Here are the main strategies to handle errors while using Promises, along with examples:

1. Using `.catch()`
    
    * The `.catch()` method is used to handle any errors that occur in the Promise chain. It catches errors thrown during the execution of the Promise, including those thrown in `.then()` handlers.
        
    * Example:
        
    
    ```javascript
    const promise = new Promise((resolve, reject) => {
      const success = false;
      if (success) {
        resolve('Operation succeeded');
      } else {
        reject('Operation failed');
      }
    });
    
    promise
      .then(result => {
        console.log(result);
      })
      .catch(error => {
        console.error('Error:', error);
      });
    ```
    
2. Using a `.catch()` after multiple `.then()`
    
    * You can chain multiple `.then()` methods and use a single `.catch()` at the end to handle errors from any of the previous `.then()` methods.
        
    * Example:
        
    
    ```javascript
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => resolve('Step 1 complete'), 1000);
    });
    
    promise
      .then(result => {
        console.log(result);
        return 'Step 2 complete';
      })
      .then(result => {
        console.log(result);
        throw new Error('Something went wrong in Step 3');
      })
      .then(result => {
        console.log(result);
      })
      .catch(error => {
        console.error('Error:', error.message);
      });
    ```
    

* In this example, the error thrown in the second `.then()` will be caught by the `.catch()` method at the end of the chain.
    

3. Handling errors in Async/Await
    
    * When using `async/await`, you can handle errors using `try/catch` blocks. This makes the code more readable and allows handling errors similarly to synchronous code.
        
    * Example:
        
    
    ```javascript
    async function fetchData() {
      try {
        let response = await fetch('https://api.example.com/data');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        let data = await response.json();
        console.log(data);
      } catch (error) {
        console.error('Fetch error:', error);
      }
    }
    
    fetchData();
    ```
    
    * In this example, any error that occurs during the fetching or processing of the data will be caught by the `catch` block.
        

### Async-Await

Using `async` and `await` in JavaScript allows you to write asynchronous code in a more synchronous and readable manner.

* `async`: This keyword is used to declare an asynchronous function. An `async` function always returns a Promise.
    
* `await`: This keyword is used to pause the execution of an `async` function until the Promise is resolved or rejected. It can only be used inside `async` functions.
    

Example:

```javascript
async function fetchData() {
  try {
    let response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

fetchData();
```

#### Handling Errors

Wrap your `await` statements in a `try/catch` block to handle errors gracefully.

Example:

```javascript
async function fetchDataWithErrorHandling() {
  try {
    let response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

fetchDataWithErrorHandling();
```

### Async-Await vs Promises

Both Promises and `async/await` are used to handle asynchronous operations in JavaScript, but they offer different syntactic approaches and have some differences in usage and readability. Here's a comparison:

1. **Syntax and Readability**
    

Promises:

* Promises use a chaining method with `.then()`, `.catch()`, and `.finally()` for handling asynchronous operations.
    
* This can lead to more verbose and less readable code, especially when chaining multiple asynchronous operations.
    

Example:

```javascript
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

`async/await`**:**

* `async/await` uses a more synchronous-looking syntax, making asynchronous code easier to read and write.
    
* It involves using the `await` keyword to wait for a Promise to resolve, within an `async` function.
    

Example:

```javascript
async function fetchData() {
  try {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();
```

**2\. Error Handling**

`Promises`:

* Errors are caught using the `.catch()` method.
    
* Error handling can be added at the end of a chain to catch errors from any step in the chain.
    

Example:

```javascript
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

`async/await`:

* Errors are handled using `try/catch` blocks, similar to synchronous code.
    
* This can lead to more straightforward and understandable error handling.
    

Example:

```javascript
async function fetchData() {
  try {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();
```

### **Best Ways to Avoid Nested Promises**

Nested promises can make your code hard to read and maintain. Here are some simple strategies to avoid nesting promises and keep your code clean and manageable.

**1\. Use** `async` and `await`:

* The `async` and `await` syntax in JavaScript makes working with promises more straightforward by allowing you to write asynchronous code that looks like synchronous code.
    
* Example:
    

Instead of nesting Promises:

```javascript
fetch('https://api.example.com/data1')
  .then(response1 => {
    return response1.json().then(data1 => {
      return fetch('https://api.example.com/data2').then(response2 => {
        return response2.json().then(data2 => {
          console.log(data1, data2);
        });
      });
    });
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

Use `async` and `await` to flatten the structure:

```javascript
async function fetchData() {
  try {
    let response1 = await fetch('https://api.example.com/data1');
    let data1 = await response1.json();
    let response2 = await fetch('https://api.example.com/data2');
    let data2 = await response2.json();
    console.log(data1, data2);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();
```

**2\. Use** `Promise.all()` for Parallel Promises:

* If you have multiple independent promises that can be executed in parallel, use `Promise.all` to run them concurrently and wait for all of them to resolve.
    
* Example:
    

Instead of nesting:

```javascript
fetch('https://api.example.com/data1')
  .then(response1 => response1.json())
  .then(data1 => {
    fetch('https://api.example.com/data2')
      .then(response2 => response2.json())
      .then(data2 => {
        console.log(data1, data2);
      });
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

Use `Promise.all`:

```javascript
async function fetchData() {
  try {
    let [response1, response2] = await Promise.all([
      fetch('https://api.example.com/data1'),
      fetch('https://api.example.com/data2')
    ]);

    let data1 = await response1.json();
    let data2 = await response2.json();
    console.log(data1, data2);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();
```

**3\. Chain Promises Properly**

* When you need to perform sequential asynchronous operations, ensure you chain promises properly without nesting.
    
* Example:
    

Instead of:

```javascript
fetch('https://api.example.com/data')
  .then(response => {
    return response.json().then(data => {
      return processData(data).then(result => {
        console.log(result);
      });
    });
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

Chain them properly:

```javascript
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => processData(data))
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

**4\. Modularize Your Code**

* Break down your code into smaller, reusable functions that return promises. This helps keep your main logic clean and readable.
    
* Example:
    

Instead of having everything in one place:

```javascript
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    return fetch('https://api.example.com/process', {
      method: 'POST',
      body: JSON.stringify(data)
    }).then(response => response.json());
  })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

Create reusable functions:

```javascript
async function getData(url) {
  let response = await fetch(url);
  return response.json();
}

async function processData(url, data) {
  let response = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(data)
  });
  return response.json();
}

async function main() {
  try {
    let data = await getData('https://api.example.com/data');
    let result = await processData('https://api.example.com/process', data);
    console.log(result);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();
```

### Conclusion

Learning asynchronous programming in JavaScript can be challenging, but understanding the tools at your disposal—such as promises and `async/await` syntax can significantly simplify the task. By leveraging techniques to avoid nested promises, such as proper chaining, using `Promise.all`, and modularizing your code, you can write cleaner, more maintainable, and more readable asynchronous code.

---

Happy Coding!!
