Asynchronous JavaScript
How JS executes the code?
The browser's JavaScript engine then creates a special environment to handle the transformation and execution of this JavaScript code. This environment is known as the Execution Context
.
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 copied 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 Execution Context.
Single-Threaded: JavaScript is single-threaded, meaning it has only one call stack and one thread of execution. This means that JavaScript code is executed sequentially, one statement at a time, from top to bottom.
Call Stack: The call stack is a data structure that stores information about the execution of functions in a program. When a function is called, a frame containing information about the function call is pushed onto the call stack. When the function returns, the frame is popped off the stack.
Difference between Sync and Async
Synchronous (Sync):
In synchronous operations, each task is executed one after the other in a sequential manner.
The program waits for each task to complete before moving on to the next one.
Synchronous code is straightforward to understand and reason about because the execution flow is predictable and linear.
Here's an example of synchronous code that reads a file and prints its contents:
// Synchronous function that adds two numbers
function add(a, b) {
return a + b;
}
// Synchronous code that calls the add function
const result = add(5, 3);
console.log("Result:", result);
- Asynchronous (Async):
In asynchronous operations, tasks are initiated without waiting for the previous task to complete.
The program continues to execute other tasks while waiting for asynchronous operations to complete.
Callback functions, promises, and async/await syntax are commonly used in JavaScript to handle asynchronous operations.
Here's an example of asynchronous code that reads a file and prints its contents:
// Asynchronous function that resolves after a delay function asyncOperation() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Async operation completed'); }, 3000); // Simulating a delay of 3 seconds }); } // Calling the asynchronous function console.log('Start of script'); asyncOperation().then((result) => { console.log(result); // Logs 'Async operation completed' after 3 seconds }); console.log('End of script');
How can we make sync code into async?
To convert synchronous code into asynchronous code in JavaScript, you typically use techniques like callbacks, promises, or async/await. Here's how you can refactor synchronous code to asynchronous code using each of these approaches:
Using Callbacks:
Rewrite the synchronous function to accept a callback function as an argument.
Inside the function, perform the asynchronous operation (e.g., reading a file) and call the callback function with the result once the operation is complete.
// Synchronous function
function syncFunction() {
return result;
}
// Asynchronous function using callback
function asyncFunction(callback) {
setTimeout(() => {
const result = syncFunction(); // Perform synchronous operation
callback(result); // Call the callback function with the result
}, 1000);
}
asyncFunction((result) => {
console.log(result);
});
Using Promises:
Wrap the asynchronous operation inside a Promise constructor.
Resolve or reject the promise based on the outcome of the operation.
// Asynchronous function using promises
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
// Perform asynchronous task
const result = syncFunction(); // Perform synchronous operation
resolve(result); // Resolve the promise with the result
} catch (err) {
reject(err); // Reject the promise with an error
}
}, 1000);
});
}
asyncFunction()
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
Using Async/Await:
Declare the function as async.
Use the await keyword before the asynchronous operation to pause the execution until the operation is complete.
// Asynchronous function using async/await
async function asyncFunction() {
try {
// Perform asynchronous task
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve(syncFunction()); // Perform synchronous operation
}, 1000);
});
return result;
} catch (err) {
throw err;
}
}
async function executeAsyncFunction() {
try {
const result = await asyncFunction();
console.log(result);
} catch (err) {
console.error(err);
}
}
executeAsyncFunction();
What are callback & what are the drawbacks of using callbacks?
Callback:
A callback is a function passed as an argument to another function, which is then invoked or called back later when a certain task or operation completes.
function fetchData(callback) {
// Simulate fetching data from a server asynchronously
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
callback(null, data); // Call the callback function with the fetched data
}, 1000);
}
// Define a callback function to handle the fetched data
function handleData(error, data) {
if (error) {
console.error('Error:', error);
} else {
console.log('Fetched data:', data);
}
}
fetchData(handleData);
Drawbacks of using callbacks:
- Callback Hell (Pyramid of Doom): When dealing with multiple asynchronous operations, nested callbacks can lead to unreadable and unmaintainable code.
- Difficulty in Error Handling: Error handling with callbacks can become cumbersome, especially in nested callback structures.
- Readability and Maintainability: Code written with callbacks can be difficult to read and understand, particularly when dealing with complex asynchronous workflows.
How promises solves the problem of inversion of control?
Key features of promises that help solve the inversion of control problem include:
Asynchronous Operations: Promises are designed to work seamlessly with asynchronous operations. They provide a way to handle the completion or failure of asynchronous tasks without blocking the main execution thread.
Chaining: Promises support method chaining, allowing multiple asynchronous operations to be sequenced and executed in a linear fashion.
Error Handling: Promises have built-in error handling mechanisms, such as the
.catch()
method, which allows developers to handle errors in a centralized and consistent way.Composition: Promises can be composed together using methods like
Promise.all()
andPromise.race()
, enabling more advanced patterns for handling multiple asynchronous operations concurrently or in parallel.
What is event loop?
Event Loop: The event loop continuously monitors the call stack and the task queue. When the call stack is empty , the event loop checks if there are any tasks in the task queue. If there are, it moves the first task from the queue to the call stack, where it's executed. This process continues indefinitely, allowing asynchronous tasks to be processed in the order they were added to the queue.
What are different functions in promises?
Promise.all(): The Promise.all()
method takes an array of promises as input and returns a single promise. This new promise is fulfilled when all input promises are fulfilled, or rejected if any of the input promises are rejected.
const p1 = Promise.resolve(5);
const p2 = new Promise ((resolve,reject) => {
setTimeout(() => {
resolve(2000);
}, 50);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 100);
});
Promise.all([p1, p2, p3]).then((values) => {
console.log(values); // [3, 2000, "foo"]
});
Promise.race(): The Promise.race()
static method takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles.
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// Both resolve, but promise2 is faster
});
// Expected output: "two"
Promise.allsettled(): Promise.allSettled
is a method in JavaScript that takes an array of promises and returns a promise that resolves after all of the given promises have either resolved or rejected.
Promise.allSettled([
Promise.resolve(33),
new Promise((resolve) => setTimeout(() => resolve(66), 0)),
99,
Promise.reject(new Error("an error")),
]).then((values) => console.log(values));
// [
// { status: 'fulfilled', value: 33 },
// { status: 'fulfilled', value: 66 },
// { status: 'fulfilled', value: 99 },
// { status: 'rejected', reason: Error: an error }
// ]
Promise.any(): It takes an iterable of Promise objects and returns a single Promise that resolves as soon as any of the promises in the iterable resolve, or rejects if all of the promises are rejected.
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Promise 1 resolved'), 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject('Promise 2 rejected'), 500);
});
Promise.any([promise1, promise2])
.then((value) => {
console.log(value); // Logs the value of the first resolved promise
})
.catch((error) => {
console.error(error); // Logs if all promises are rejected
});
// output: Promise 1 resolved