Asynchronous-javascript
How is JS code being executed?
JavaScript is a single-threaded language that can execute one task at a time.
The JavaScript engine creates a Call Stack, which is a data structure used to keep track of the execution context (functions being executed).
When a script starts running, the Global Execution Context is pushed onto the Call Stack.
Whenever a function is called, a new execution context is created and pushed onto the Call Stack.
The engine executes the code in the current execution context until it encounters a blocking operation (e.g., an AJAX request, a timer, or an event listener).
For blocking operations, the engine offloads the task to the Web APIs, which are provided by the browser.
Once the Web API task is completed, it gets pushed into the Task Queue (also known as the Callback Queue).
The Event Loop continuously checks the Call Stack and the Task Queue. If the Call Stack is empty, it takes the first task from the Task Queue and pushes it onto the Call Stack for execution.
This execution model ensures that JavaScript code is executed in a non-blocking way, allowing the browser to remain responsive even when handling long-running tasks.
What is the difference between Sync & Async?
Synchronous code is executed in a sequential manner, where each operation must wait for the previous one to complete before moving on to the next. In synchronous execution, the program is blocked until the current operation finishes, and the control is not returned to the event loop until the operation is complete.
Asynchronous code, on the other hand, allows the program to execute non-blocking operations and move on to the next task without waiting for the previous one to finish. Instead of blocking the execution, asynchronous operations are offloaded to the Web APIs, and when they complete, they are added to the Task Queue to be executed later by the Event Loop. Here's an example to illustrate the difference:
// Synchronous code
console.log("Start");
console.log("Middle");
console.log("End");
// Output: Start, Middle, End
// Asynchronous code
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
// Output: Start, End, Timeout
In the synchronous code, the output is printed in the expected order (Start
, Middle
, End
). However, in the asynchronous code, the setTimeout
function is a non-blocking operation that is offloaded to the Web APIs. The "End"
log is printed before "Timeout"
because setTimeout
is asynchronous and doesn't block the execution of the next line.
What is a call stack queue?
- The Call Stack is a data structure that keeps track of the execution contexts (functions being executed) in the order they were called. When a function is called, its execution context is pushed onto the Call Stack. When the function finishes executing, its execution context is popped off the Call Stack. The Queue (also known as the Task Queue or Callback Queue) is a data structure that holds the asynchronous tasks waiting to be executed. When an asynchronous operation completes, its callback function is added to the Task Queue, waiting for its turn to be pushed onto the Call Stack and executed by the Event Loop. Here's an example to illustrate the Call Stack and Task Queue:
function main() {
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
}
main();
// Output: Start, End, Timeout
- When the
main
function is called, its execution context is pushed onto the Call Stack. The"Start"
log is printed, and then thesetTimeout
function is offloaded to the Web APIs. The"End"
log is printed, and themain
execution context is popped off the Call Stack. Once the timer expires, the callback function fromsetTimeout
is added to the Task Queue. When the Call Stack is empty, the Event Loop takes the callback from the Task Queue and pushes it onto the Call Stack for execution, resulting in the"Timeout"
log.
What is an event loop?
- The Event Loop is a mechanism that continuously checks two things:
If the Call Stack is empty.
If there are any tasks in the Task Queue.
- If the Call Stack is empty and there are tasks in the Task Queue, the Event Loop takes the first task from the Task Queue and pushes it onto the Call Stack for execution. The Event Loop is responsible for ensuring that the JavaScript engine can handle asynchronous tasks efficiently, without blocking the main execution thread. It allows the engine to prioritize different types of tasks and execute them in the correct order, maintaining the non-blocking nature of JavaScript.
What is an inversion of control and what problem is caused by this?
Inversion of Control (IoC) is a design principle in software engineering where the control flow of a program is inverted. Instead of the code explicitly calling a function or creating objects, the control is transferred to an external framework or runtime environment, which then calls the code at the appropriate time. In the context of JavaScript, IoC is related to the asynchronous nature of the language and the use of callbacks. When you perform an asynchronous operation and provide a callback function, you are essentially handing over control to the JavaScript runtime environment. The runtime will execute your code, and when the asynchronous operation completes, it will call your callback function. The problem that arises with this approach is known as Callback Hell or the Pyramid of Doom. Callback Hell occurs when you have nested callbacks within callbacks, leading to deeply nested and hard-to-read code. This can make the code difficult to maintain and reason about, especially when dealing with multiple asynchronous operations or error handling. Here's an example of Callback Hell:
getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { getMoreData(c, function(d) { getMoreData(d, function(e) { // Do something with the data }); }); }); }); });
As you can see, the code becomes deeply nested and difficult to read and maintain as the number of asynchronous operations increases. This is where Promises and async/await come into play, providing more elegant solutions to handle asynchronous code and avoid Callback Hell.
How to avoid callback hell?
There are two primary ways to avoid Callback Hell in JavaScript:
Promises: Promises provide a more structured and readable way to handle asynchronous operations. Instead of nesting callbacks, you can chain Promise methods like
.then()
and.catch()
to handle the asynchronous flow.Async/await: Introduced in ES2017, async/await is a syntactical sugar built on top of Promises. It allows you to write asynchronous code that looks and behaves more like synchronous code, making it easier to read and understand.
Here's an example of how to avoid Callback Hell using Promises:
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => getMoreData(d))
.then(e => {
// Do something with the data
})
.catch(error => {
// Handle errors
What is a promise? And what problem is it actually solving
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It provides a more structured and readable way to handle asynchronous code in JavaScript, addressing the issues of Callback Hell. Promises solve several problems associated with callbacks: 1 1 1. Callback Nesting: Promises allow you to chain asynchronous operations using the
.then()
method, avoiding deeply nested callbacks.2. Error Handling: Promises have a dedicated
.catch()
method for handling errors, providing a centralized and more manageable way of error handling.3. Clarity and Readability: Promise chains are easier to read and understand compared to nested callbacks, making the code more maintainable.
Promises have three states:
Pending: The initial state, when the asynchronous operation has not completed yet.
Fulfilled: The state when the asynchronous operation is successful, and the Promise is resolved with a value.
Rejected: The state when the asynchronous operation fails, and the Promise is rejected with a reason (error).
Here's an example of creating and using a Promise:
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber < 0.5) {
resolve(`Success! Random number: ${randomNumber}`);
} else {
reject(`Failure! Random number: ${randomNumber}`);
}
}, 2000);
});
// Using the Promise
myPromise
.then(result => console.log(result)) // Handles the resolved Promise
.catch(error => console.log(error)); // Handles the rejected Promise
In this example, we create a Promise that simulates an asynchronous operation using setTimeout
. After 2 seconds, it randomly resolves or rejects the Promise based on a random number. We then use the .then()
method to handle the resolved Promise, and the .catch()
method to handle the rejected Promise.
How to create a promise?
To create a Promise, you use the
Promise
constructor and pass a callback function called the executor. The executor function takes two arguments:resolve
andreject
, which are functions provided by the JavaScript engine. Here's the syntax for creating a Promise:const myPromise = new Promise((resolve, reject) => { // Asynchronous operation // ... if (/* condition for success */) { resolve(value); // Resolve the Promise with a value } else { reject(error); // Reject the Promise with an error } });
Inside the executor function, you perform your asynchronous operation. If the operation is successful, you call the
resolve
function with the desired value. If the operation fails, you call thereject
function with an error value. Here's an example of creating a Promise that resolves after a 2-second delay:const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Success!'); }, 2000); }); myPromise.then(result => console.log(result)); // Output: Success!
In this example, we create a Promise that resolves with the string
'Success!'
after a 2-second delay usingsetTimeout
. We then use the.then()
method to handle the resolved Promise and log the result to the console.
What are web browser APIs?
- Web Browser APIs are interfaces provided by web browsers that allow JavaScript code to interact with various browser features and functionalities. These APIs expose methods, properties, and events that enable developers to create dynamic and interactive web applications. Some examples of Web Browser APIs include:
DOM (Document Object Model) API: Allows manipulation of HTML and XML documents, including adding, modifying, and removing elements.
Web Storage API: Provides a way to store and retrieve data in the browser, including local storage and session storage.
Fetch API: Allows making HTTP requests to fetch resources asynchronously.
Canvas API: Allows rendering 2D and 3D graphics on a canvas element.
Web Audio API: Provides a way to generate, process, and analyze audio data in the browser.
Geolocation API: Allows retrieving the user's geographical location.
Web Sockets API: Enables two-way communication between the client and server over a persistent connection.
Web Workers API: Allows running scripts in background threads, enabling parallel processing.
- These APIs are used extensively in modern web development to create rich and interactive user experiences. For example, the Fetch API is commonly used to make AJAX requests and retrieve data from servers, while the Canvas API is used for creating games, visualizations, and animations. Web Browser APIs are asynchronous by nature, meaning that they often rely on callbacks, Promises, or async/await to handle the results of their operations.
What is a micro-task queue?
The Microtask Queue is a queue that exists alongside the Task Queue (also known as the Callback Queue or Macrotask Queue) in the JavaScript Event Loop. It is used to handle specific types of asynchronous tasks called microtasks. Microtasks include:
Promises (resolved or rejected)
Promise.resolve()
queueMicrotask()
MutationObserver
callbacks
The key difference between the Microtask Queue and the Task Queue is the order in which they are processed by the Event Loop. The Event Loop gives higher priority to the Microtask Queue and processes all microtasks before moving on to the Task Queue. Here's a visual representation of the Event Loop with the Microtask Queue:
The Event Loop processes tasks in the following order:
Execute all tasks in the Call Stack.
If the Call Stack is empty, execute all microtasks in the Microtask Queue.
If the Microtask Queue is empty, execute the next task from the Task Queue.
This prioritization ensures that Promises and other microtasks are handled immediately after the current code finishes executing, without being interrupted by other tasks in the Task Queue. Here's an example that illustrates the order of execution with the Microtask Queue:
console.log('Start'); setTimeout(() => { console.log('Timeout'); }, 0); Promise.resolve('Resolved Promise').then(result => { console.log(result); }); console.log('End'); // Output: // Start // End // Resolved Promise // Timeout
In this example, the
Promise.resolve('Resolved Promise')
is a microtask and is executed before the task fromsetTimeout
in the Task Queue. Therefore, the output shows"Resolved Promise"
before"Timeout"
.
Different functions in Promise
Promises provide several methods and properties to handle asynchronous operations effectively. Here are some of the commonly used functions:
Promise.resolve(value)
: Creates a resolved Promise with the given value.Promise.reject(reason)
: Creates a rejected Promise with the given reason.promise.then(onFulfilled, onRejected)
: Attaches callbacks for when the Promise is resolved or rejected.promise.catch(onRejected)
: Attaches a callback for when the Promise is rejected (shorthand forpromise.then(null, onRejected)
).Promise.all(iterable)
: Returns a new Promise that resolves when all Promises in the iterable are resolved or rejects with the reason of the first rejected Promise.Promise.race(iterable)
: Returns a new Promise that resolves or rejects as soon as one of the Promises in the iterable is resolved or rejected.promise.finally(onFinally)
: Attaches a callback that will be executed when the Promise is settled (resolved or rejected).
Here are some examples:
// Promise.resolve()
const resolvedPromise = Promise.resolve('Success');
resolvedPromise.then(result => console.log(result)); // Output: Success
// Promise.reject()
const rejectedPromise = Promise.reject('Error');
rejectedPromise.catch(error => console.log(error)); // Output: Error
// promise.then()
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Resolved after 2 seconds');
}, 2000);
});
myPromise.then(
result => console.log(result), // Handles the resolved Promise
error => console.log(error) // Handles the rejected Promise
);
// Promise.all()
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'Promise 3');
});
Promise.all([promise1, promise2, promise3]).then(values => {
console.log(values); // Output: [3, 42, 'Promise 3']
});
// Promise.race()
const promiseA = new Promise(resolve => setTimeout(resolve, 2000, 'Promise A'));
const promiseB = new Promise(resolve => setTimeout(resolve, 1000, 'Promise B'));
Promise.race([promiseA, promiseB]).then(result => {
console.log(result); // Output: 'Promise B' (because it resolves first)
});
How to handle errors while using promises
Handling errors is an essential part of working with Promises. Promises provide a dedicated method,
.catch()
, to handle errors that occur during the asynchronous operation or within the Promise chain. Here's an example of how to handle errors with Promises:const myPromise = new Promise((resolve, reject) => { // Simulating an error reject('Something went wrong'); }); myPromise .then(result => { console.log(result); // This part is skipped because the Promise is rejected }) .catch(error => { console.log(error); // Output: 'Something went wrong' });
In this example, we create a Promise that immediately rejects with the reason 'Something went wrong'
. The .catch()
method is used to handle the rejection and log the error. You can also chain multiple .catch()
blocks to handle errors at different stages of the Promise chain:
getData()
.then(processData)
.then(displayData)
.catch(error => {
console.log('Error occurred:', error);
// You can also do additional error handling or logging here
});
In this example, if an error occurs at any stage of the Promise chain (in getData()
, processData()
, or displayData()
), it will be caught and handled by the .catch()
block. Another best practice is to always include a .catch()
block at the end of your Promise chain to handle unhandled rejections:
getData()
.then(processData)
.then(displayData)
.catch(error => {
console.log('Error occurred:', error);
})
.finally(() => {
// Cleanup or additional logic here
});
The .finally()
method is executed regardless of whether the Promise is resolved or rejected, making it useful for cleanup tasks or additional logic that should run after the Promise chain is completed.
How to use Async Await
Async/Await is a modern syntax introduced in ES2017 (ES8) that provides a more straightforward and readable way to work with Promises. It allows you to write asynchronous code that looks and behaves more like synchronous code, making it easier to understand and reason about. Here's how you can use async/await
:
Define an async function To use
await
, you need to define anasync
function. Anasync
function always returns a Promise, even if you don't explicitly return one.javascriptCopy codeasync function myAsyncFunction() { // Asynchronous code goes here }
Use the
await
keyword Inside anasync
function, you can use theawait
keyword to pause the execution of the function until the Promise is resolved or rejected. Theawait
keyword can only be used inside anasync
function.async function fetchData() { try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); } catch (error) { console.error('Error:', error); } } fetchData();
In this example, the
await
keyword is used to wait for thefetch
request to complete and the response to be converted to JSON. The code inside thetry
block will pause execution until the Promise is resolved or rejected.Handle errors with
try...catch
When usingasync/await
, you can handle errors using a standardtry...catch
block, just like you would with synchronous code.javascriptCopy codeasync function fetchData() { try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); } catch (error) { console.error('Error:', error); } }
If an error occurs during the asynchronous operation, it will be caught by the
catch
block, making error handling more straightforward.Using
async/await
can make your asynchronous code more readable and easier to reason about, especially when dealing with multiple Promises or nested Promises.
Different between promise & async..await
While Promises and async/await
are both used to handle asynchronous operations in JavaScript, there are some fundamental differences between them: Promises:
Promises are an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
Promises provide a more structured way to handle asynchronous code compared to callbacks, using
.then()
and.catch()
methods.Promises can be chained together using
.then()
, allowing for sequential or parallel execution of asynchronous operations.Promises have a clear separation between the asynchronous operation and the handling of the result/error.
async/await:
async/await
is a syntax sugar built on top of Promises, providing a more intuitive and synchronous-like way to work with asynchronous code.The
async
keyword is used to define an asynchronous function that implicitly returns a Promise.The
await
keyword is used inside anasync
function to pause the execution until the Promise is resolved or rejected.async/await
allows you to write asynchronous code that looks and behaves more like synchronous code, making it easier to read and maintain.Error handling with
async/await
can be done using standardtry...catch
blocks, similar to synchronous code.
Best ways to avoid nested promises
- Use Promise Chaining
Instead of nesting Promises inside each other, you can chain them together using the .then()
method. This makes the code more readable and easier to follow.
javascriptCopy codegetData()
.then(data => {
return processData(data);
})
.then(processedData => {
return saveData(processedData);
})
.then(savedData => {
displayData(savedData);
})
.catch(error => {
handleError(error);
});
- Use async/await
async/await
provides a more straightforward and readable way to work with Promises, eliminating the need for nested Promises.
javascriptCopy codeasync function fetchAndProcessData() {
try {
const data = await getData();
const processedData = await processData(data);
const savedData = await saveData(processedData);
displayData(savedData);
} catch (error) {
handleError(error);
}
}
fetchAndProcessData();
- Use Promise Utilities
JavaScript provides built-in Promise utilities like Promise.all()
, Promise.allSettled()
, and Promise.race()
that can help you avoid nesting.
Example using Promise.all()
:
javascriptCopy codeconst promise1 = getData();
const promise2 = getMoreData();
const promise3 = getEvenMoreData();
Promise.all([promise1, promise2, promise3])
.then(([data1, data2, data3]) => {
const processedData = processData(data1, data2, data3);
return processedData;
})
.then(processedData => saveData(processedData))
.then(savedData => displayData(savedData))
.catch(error => handleError(error));