Welcome to our JavaScript Interview Series! Asynchronous operations are the bedrock of modern web development, and a deep understanding of how they work is a must for any serious developer. Interviewers love to probe into Promises, async/await, and the Event Loop to gauge your understanding of JavaScript's concurrency model.
Get ready to level up. Weβve compiled 10 essential questions that move from foundational concepts to advanced scenarios. Let's dive in and make sure you're prepared to ace your next technical interview.
1. What is the Event Loop and how does it work?
Key Assessment Point: This question tests your fundamental understanding of JavaScript's non-blocking concurrency model.
Standard Answer:
JavaScript is a single-threaded language, meaning it can only execute one task at a time. However, it handles asynchronous operations (like API calls or timers) without freezing the main thread. This is managed by the Event Loop.
Here's a simplified breakdown of how it works:
- Call Stack: When code is executed, functions are pushed onto the call stack.
- Web APIs/Node.js APIs: When an asynchronous operation like
setTimeoutor afetchrequest is encountered, it's handed off to a Web API (in the browser) or a C++ API (in Node.js). It does not block the stack. - Callback Queue (or Task Queue): Once the asynchronous operation is complete, its callback function is placed in the Callback Queue.
- Event Loop: The Event Loop's primary job is to continuously check if the Call Stack is empty. If it is, it takes the first callback from the Callback Queue and pushes it onto the Call Stack for execution.
This entire process ensures that time-consuming operations don't block the main thread, keeping the user interface responsive.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What's the difference between the Callback Queue (for macrotasks) and the Microtask Queue?
- Can you explain a scenario where a long-running task on the Call Stack could still block the Event Loop?
- How does the Event Loop in Node.js differ from the one in the browser?
2. What is a Promise? Can you explain its states?
Key Assessment Point: This question assesses your knowledge of the core building block for modern asynchronous operations in JavaScript.
Standard Answer:
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It's a placeholder for a value you don't have yet. This allows you to associate handlers with an asynchronous action's eventual success value or failure reason.
A Promise can be in one of three states:
- Pending: The initial state; the operation has not yet completed.
- Fulfilled (or Resolved): The operation completed successfully, and the Promise now has a resulting value.
- Rejected: The operation failed, and the Promise has a reason for the failure.
Once a Promise is either fulfilled or rejected, it is settled and becomes immutable; its state cannot change again.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What is "Promise chaining," and why is it useful?
- How do you create a new Promise from scratch using the
new Promise()constructor? - Can a Promise move from a fulfilled state back to a pending state? Why or why not?
3. What are the differences between Microtasks and Macrotasks?
Key Assessment Point: This tests a deeper understanding of the Event Loop's execution order and priority.
Standard Answer:
The Event Loop actually manages two different queues: the Microtask Queue and the Macrotask Queue (also known as the Callback Queue).
- Macrotasks: These include tasks like
setTimeout,setInterval, I/O operations, and UI rendering. Each macrotask is processed from the Macrotask Queue only after the Call Stack is empty. After a macrotask runs, the Event Loop moves on. - Microtasks: These include tasks like
Promise.then(),.catch(),.finally(),async/await, andqueueMicrotask(). The Microtask Queue has a higher priority.
The key rule is: After each macrotask is completed, the Event Loop processes the entire Microtask Queue before moving on to the next macrotask. This means if a microtask adds another microtask to the queue, it will also run before the next macrotask, which can potentially starve the macrotask queue and delay things like rendering.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- If you have a
setTimeoutwith a 0ms delay and a resolved Promise's.then(), which one's callback will execute first and why? - Can you describe a scenario where recursively adding microtasks could cause performance issues?
- Where do
requestAnimationFramecallbacks fit into this model?
4. What is async/await and how does it relate to Promises?
Key Assessment Point: Evaluates your ability to work with the modern, more readable syntax for handling asynchronous code.
Standard Answer:
async/await is syntactic sugar built on top of Promises, designed to make asynchronous code look and behave more like synchronous code, which makes it easier to read and maintain.
-
asyncfunction: Theasynckeyword is used to declare a function as asynchronous. It implicitly returns a Promise. If the function returns a value, the Promise will be resolved with that value. If the function throws an error, the Promise will be rejected with that error. -
awaitoperator: Theawaitkeyword can only be used inside anasyncfunction. It pauses the execution of the function until the Promise it is waiting on is settled (either fulfilled or rejected). If the Promise is fulfilled,awaitreturns the fulfilled value. If it's rejected, it throws the rejection reason as an error.
Crucially, await does not block the main thread. It only pauses the execution of the async function it's in, allowing other code to run until the awaited Promise settles.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What happens if you use
awaiton a value that is not a Promise? - How do you handle errors in
async/awaitfunctions? - Can you convert a Promise chain using
.then()and.catch()into an equivalentasync/awaitfunction?
5. What is "callback hell" and how do Promises and async/await solve it?
Key Assessment Point: Checks if you understand the historical context and the practical benefits of modern asynchronous patterns.
Standard Answer:
"Callback hell" (also known as the "pyramid of doom") describes a situation where multiple nested callbacks make the code hard to read, debug, and maintain. This often happens when you have a sequence of dependent asynchronous operations.
Promises solve this by introducing .then() chaining. Instead of nesting, you can chain .then() handlers, which flattens the code structure and makes it sequential and much more readable. Each .then() returns a new Promise, allowing the chain to continue.
async/await solves it even more elegantly. It allows you to write the same asynchronous logic as if it were synchronous. You simply await the result of each asynchronous operation one after another within an async function, completely eliminating the need for nested callbacks or even long .then() chains.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- Besides readability, what other issues does callback hell present? (e.g., error handling)
- Can you still get into a form of "hell" with
async/awaitif you're not careful? (e.g., awaiting promises in sequence when they could be run in parallel) - Show me a code snippet of callback hell and refactor it using Promises, and then again using
async/await.
6. Explain the difference between Promise.all() and Promise.race().
Key Assessment Point: Tests your knowledge of Promise combinators for handling multiple concurrent operations.
Standard Answer:
Both Promise.all() and Promise.race() are methods that take an iterable (like an array) of Promises as input, but they behave differently:
-
Promise.all(): This method is for when you need all the promises to complete successfully.- It returns a single Promise that fulfills when all of the input Promises have fulfilled. The fulfilled value is an array of the results from the input Promises, in the same order.
- It fails fast. If any of the input Promises reject, the
Promise.all()immediately rejects with the reason of the first Promise that rejected.
-
Promise.race(): This method is for when you only care about the first promise to settle.- It returns a single Promise that settles (either fulfills or rejects) as soon as the very first Promise in the iterable settles. The resulting Promise will take on the state and value of that first settled Promise.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What is
Promise.allSettled()and how is it different fromPromise.all()? When would you use it? - What happens if you pass an empty array to
Promise.all()? What aboutPromise.race()? - Can you write a simple implementation of
Promise.race()yourself?
7. How would you handle errors for a fetch call within an async function?
Key Assessment Point: This practical question assesses your understanding of error handling in async/await, including the nuances of specific APIs like fetch.
Standard Answer:
The most common and robust way to handle errors in an async function is with a try...catch block.
async function fetchData(url) {
try {
const response = await fetch(url);
// The fetch API does not throw an error on bad HTTP statuses (like 404 or 500).
// So, we must check the `response.ok` property and manually throw an error.
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
// This will catch network errors (e.g., DNS failure) and the error we threw above.
console.error('Failed to fetch data:', error);
}
}
A key point to remember with fetch is that it only rejects its Promise on a network failure. For bad HTTP responses like 404 (Not Found) or 500 (Internal Server Error), the Promise still fulfills. Therefore, you must manually check response.ok or response.status and throw an error to be caught by the catch block.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What if you don't use
try...catch? How would the caller of thisasyncfunction handle a potential rejection? - How would this error handling differ if you were using a library like Axios instead of
fetch? - Can you add a
finallyblock to this example and explain what it would be used for?
8. What will be logged to the console in the following code snippet?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
Key Assessment Point: This is a classic question to precisely test your understanding of the execution order of synchronous code, microtasks, and macrotasks.
Standard Answer:
The output will be:
A
D
C
B
Explanation:
-
console.log('A'): This is synchronous code and executes immediately. 'A' is logged. -
setTimeout(() => console.log('B'), 0): This is a macrotask. The callback function() => console.log('B')is sent to the Web API and then placed in the Macrotask Queue after its 0ms delay. -
Promise.resolve().then(() => console.log('C')): The.then()callback is a microtask. It is immediately placed in the Microtask Queue. -
console.log('D'): This is synchronous code and executes immediately. 'D' is logged. - The initial script execution is finished, and the Call Stack is now empty.
- The Event Loop checks the Microtask Queue first. It finds the callback for 'C' and executes it. 'C' is logged.
- The Microtask Queue is now empty. The Event Loop then checks the Macrotask Queue. It finds the callback for 'B' and executes it. 'B' is logged.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- What would happen if we added another
.then()to the Promise chain? - How would the output change if we used
queueMicrotask()instead of a Promise? - Create a more complex version of this puzzle involving nested
setTimeoutandPromise.then()calls.
9. Can you create a Promise that resolves after a specific amount of time?
Key Assessment Point: This tests your ability to wrap traditional callback-based asynchronous functions (like setTimeout) within a Promise.
Standard Answer:
Yes, you can do this by wrapping setTimeout inside the Promise constructor. The constructor takes an executor function with two arguments: resolve and reject. You call resolve when the asynchronous operation is successful.
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
// Usage:
async function sayHello() {
console.log('Starting...');
await delay(2000); // Pauses here for 2 seconds
console.log('Hello after 2 seconds');
}
sayHello();
In this delay function, we create a new Promise. Inside it, we call setTimeout. When the timer of ms milliseconds completes, it will call the resolve function, which in turn fulfills the Promise.
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- How could you modify this function to also be able to reject the promise, for example, with a
cancelfunction? - What is the benefit of "promisifying" a function like this compared to just using
setTimeoutwith a callback? - Can you write a more generic "promisify" function that can convert any callback-based function (that follows the error-first callback pattern) into a Promise-based one?
10. How would you implement Promise.all() from scratch?
Key Assessment Point: This advanced question truly tests your deep understanding of Promise mechanics, state management, and handling multiple asynchronous results.
Standard Answer:
To implement Promise.all(), you need to handle several key aspects: it takes an array of promises, tracks when all of them have completed, stores their results in the correct order, and handles the case where one of them rejects.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completedCount = 0;
const totalPromises = promises.length;
// Handle empty iterable case
if (totalPromises === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
// Ensure we are working with a promise
Promise.resolve(promise)
.then(value => {
results[index] = value;
completedCount++;
if (completedCount === totalPromises) {
resolve(results);
}
})
.catch(error => {
// Reject immediately on the first error
reject(error);
});
});
});
}
Explanation:
- We return a new Promise that will be the final result.
- We initialize an array
resultsto store the resolved values and acompletedCountto track progress. - We iterate over the input
promises. For each promise, we use.then()to handle its successful resolution. - When a promise resolves, we store its value at the correct index in the
resultsarray to maintain order. - We increment our counter. If the counter matches the total number of promises, it means they have all fulfilled, and we can
resolveour main promise with theresultsarray. - Crucially, in the
.catch()for any promise, we immediatelyrejectthe main promise, which is the "fail-fast" behavior ofPromise.all().
Possible Follow-up Questions:
π (Want to test your skills? Try a Mock Interviews)
- Why is it important to use
Promise.resolve(promise)inside the loop? - How would your implementation change if you were creating
Promise.allSettled()instead? - What are the edge cases you've handled in this implementation? (e.g., empty array, non-promise values in the array).
Top comments (0)