All about Javascript Promises for Interviews | Output Driven Questions

--

Please SUBSCRIBE My YouTube Channel: FrontEnd Interview Preparation: https://www.youtube.com/channel/UC-elmWUfbcbmvuhlS12nCtg

👏 Please clap for the story and follow me 👉

Callback

  • Callbacks are functions passed as arguments to another function, and they are executed once the operation is completed.
  • Callbacks are a common pattern for handling asynchronous operations in older JavaScript code.
function fetchData(callback) {
// Simulating an asynchronous operation
setTimeout(function() {
const data = "Some data";
callback(null, data); // Passing null for error and data for success
}, 1000);
}

// Using the fetchData function with a callback
fetchData(function(error, data) {
if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});

One drawback of callbacks is the potential for callback hell, where nested callbacks make the code harder to read and maintain.

Callback Hell

Callback hell, also known as “pyramid of doom,” occurs when multiple nested callbacks are used, leading to code that is difficult to read and maintain. Here’s an example of callback hell:

  • This is one of the problems that Promises aim to solve, providing a more structured and readable way to handle asynchronous code without falling into callback hell.

EXAMPLE 1:
function stepOne(callback) {
setTimeout(function() {
console.log("Step One completed");
callback();
}, 1000);
}

function stepTwo(callback) {
setTimeout(function() {
console.log("Step Two completed");
callback();
}, 1000);
}

function stepThree(callback) {
setTimeout(function() {
console.log("Step Three completed");
callback();
}, 1000);
}

function finalStep() {
console.log("All steps completed");
}

// Nested callbacks
stepOne(function() {
stepTwo(function() {
stepThree(function() {
finalStep();
});
});
});


EXAMPLE 2:
// Example of Callback Hell
function fetchDataFromAPI(callback1) {
setTimeout(function() {
console.log("Step 1: API data fetched");
callback1();
}, 1000);
}

function processData(data, callback2) {
setTimeout(function() {
console.log("Step 2: Data processed");
callback2();
}, 1000);
}

function displayResult(result, callback3) {
setTimeout(function() {
console.log("Step 3: Result displayed");
callback3();
}, 1000);
}

// Nested callbacks
fetchDataFromAPI(function() {
processData(data, function() {
displayResult(result, function() {
console.log("All steps completed");
});
});
});

Promises return a value which is either a resolved value or a reason why it’s rejected.

A Promise has four states

Pending: Before the event has happened, the promise is in the pending state.

Settled: Once the event has happened it is then in the settled state.

Fulfilled: Action related to the promise has succeeded.

Rejected: Action related to the promise has failed.

Comparison Table

`Promise.all()`

The Promise.all() method executes many promises in parallel. It accepts an array of promises and returns a single promise.

  • It will only resolve if all the promises passed have been resolved.
  • If any promise in an array of promises fails then it will reject.
const promiseArr = [
new Promise(resolve => setTimeout(resolve, 100, 'apple')),
new Promise(resolve => setTimeout(resolve, 100, 'banana')),
new Promise(resolve => setTimeout(resolve, 3000, 'orange'))
]
Promise.all(promiseArr)
.then(fruits => console.log(fruits))
.catch(err => console.log('Error:', err))

// After 3 seconds, logs out the following
// ["apple", "banana", "orange"]

Implement or Polyfill

/**
*
@param {Array<any>} promises - notice input might have non-Promises
*
@return {Promise<any[]>}
*/
function all(promises) {
return new Promise((resolve, reject) => {
if (!promises.length) {
resolve([])
}

const poolResponses = []
let count = 0

promises.forEach((p, idx) => {
Promise.resolve(p).then(res => {
poolResponses[idx] = res
count++
if (count === promises.length) {
resolve(poolResponses)
}
}).catch(err => {
reject(err)
})
})
})
}

`Promise.allSettled()`

Promise.allSettled() method which accepts an array of promises. It returns a new promise that will resolve if all the promises in the array are settled, regardless of whether the promises are resolved or rejected.

An example of Promise.allSettled() with one promise rejected:

const promiseArr = [
new Promise(resolve => setTimeout(resolve, 100, 'apple')),
new Promise((resolve, reject) => setTimeout(reject, 10, 'banana')),
new Promise(resolve => setTimeout(resolve, 3000, 'orange'))
]
Promise.allSettled(promiseArr)
.then(fruits => console.log(fruits))
.catch(err => console.log('Error:', err))

// After 10 ms, logs out the following
// [
// {status: 'fulfilled', value: 'apple'},
// {status: 'rejected', value: 'banana'},
// {status: 'fulfilled', value: 'orange'}
// ]

Implement or Polyfill

using promise,


/* [{
status: "fulfilled",
value: "apple"
}, {
status: "fulfilled",
value: "banana"
}, {
reason: "orange",
status: "rejected"
}] */


const allSettled = (promises) => {
if (!promises.length) {
return Promise.resolve([]);
}

const poolResponses = [];
let counter = 0;

return new Promise((resolve) => {
promises.forEach((promise, i) => {
promise
.then((res) => {
poolResponses[i] = {
status: 'fulfilled',
value: res,
};
})
.catch((err) => {
poolResponses[i] = {
status: 'rejected',
reason: err,
};
})
.finally(() => {
counter++;
if (counter === promises.length) {
resolve(poolResponses);
}
});
});
});
};

Using async or await,

/**
*
@param {Array<any>} promises - notice that input might contains non-promises
* @return {Promise<Array <{status: 'fulfilled', value: any} | {status: 'rejected', reason: any}>>} */const allSettled = async (promises) => {
if (!promises.length) {
return []
}

const poolResponses = []
let counter = 0

for (let i = 0; i < promises.length; i++) {
try {
const res = await promises[i];
poolResponses[i] = {
status: 'fulfilled',
value: res
}
} catch (err) {
poolResponses[i] = {
status: 'rejected',
reason: err
}
} finally {
counter++
if (counter === promises.length) {
return poolResponses
}
}
}
}

Implement `Promise.race()`

The Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise

Example:

const promise1 = new Promise(resolve => setTimeout(() => resolve('Promise 1 resolved'), 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Promise 2 rejected'), 500));
const promise3 = new Promise(resolve => setTimeout(() => resolve('Promise 3 resolved'), 1500));

// Using Promise.race() to determine the first settled Promise
Promise.race([promise1, promise2, promise3])
.then(result => {
console.log('First settled Promise:', result);
})
.catch(error => {
console.error('Error:', error);
});

Implementation:

/**
*
@param {Array<Promise>} promises
* @return {Promise}
*/
const
race = (promises) => {
return new Promise((resolve, reject) => {
promises.forEach(p => p.then(resolve, reject))
})
}

Auto-retry Promise on rejection

  • Fetching data in a web application is common. However, we might need retry mechanisms in place should we have Network problems.

How to implement fetchWithAutoRetry that takes in a promise and a maximumRetryCount. The util will keep retrying the promise until the counter runs out, at that time, we will reject with the last error.

PROMISE:

function fetchWithAutoRetry(promise, maximumRetryCount, currentRetryCount = 0) {
return promise().catch(error => {
if (currentRetryCount < maximumRetryCount) {
const delay = Math.pow(2, currentRetryCount) * 100;
console.log(`Retrying (${currentRetryCount + 1}/${maximumRetryCount})...`);
return new Promise(resolve =>
setTimeout(resolve, delay)
).then(() =>
fetchWithAutoRetry(promise, maximumRetryCount, currentRetryCount + 1)
);
} else {
throw error; // Reject with the last error if maximum retry count is reached
}
});
}

// Example usage:
const url = 'https://api.example.com/data';

// Simulating a failing fetch with a promise
function simulateFailingFetch() {
return Promise.reject('Failed to fetch data');
}

// Using fetchWithAutoRetry with a maximum retry count of 3
fetchWithAutoRetry(simulateFailingFetch, 3)
.then(data => {
console.log('Data fetched successfully:', data);
})
.catch(error => {
console.error('Max retries reached. Last error:', error);
});

ASYNC/AWAIT:

/**
*
@param {() => Promise<any>} promise
* @param {number} maximumRetryCount
* @return {Promise<any>}
*/
const
fetchWithAutoRetry = async (promise, maximumRetryCount) => {
try {
return await promise()
} catch (err) {
if (maximumRetryCount === 0) {
return Promise.reject(err)
}
return fetchWithAutoRetry(promise, maximumRetryCount - 1)
}
}

Throttle Promises

  • Generally, we use Promise.all or Promise.allSettled to compute promises in sequence. However, there are times that we need to throttle a list of promises dynamically.
  • Due to network or hardware limitations, or the lack of paginations, we have to limit how many concurrent requests can be done at the same time.

How to implement throttlePromises() that takes in an array of functions returning promises, and a variable denoting the maximum concurrent calls?

// Example usage:

const promises = [
() => fetch('https://jsonplaceholder.typicode.com/todos/1'),
() => fetch('https://jsonplaceholder.typicode.com/todos/2'),
() => fetch('https://jsonplaceholder.typicode.com/todos/3'),
() => fetch('https://jsonplaceholder.typicode.com/todos/4'),
() => fetch('https://jsonplaceholder.typicode.com/todos/5'),
() => fetch('https://jsonplaceholder.typicode.com/todos/6'),
() => fetch('https://jsonplaceholder.typicode.com/todos/7'),
() => fetch('https://jsonplaceholder.typicode.com/todos/8'),
() => fetch('https://jsonplaceholder.typicode.com/todos/9'),
() => fetch('https://jsonplaceholder.typicode.com/todos/10'),
() => fetch('https://jsonplaceholder.typicode.com/todos/11'),
() => fetch('https://jsonplaceholder.typicode.com/todos/12'),
// Add more promise functions as needed
];


const maxConcurrentCalls = 4;


// Using throttlePromises
throttlePromises(promises, maxConcurrentCalls)
.then(results => {
console.log('All promises completed with results:', results);
})
.catch(error => {
console.error('Error:', error);
});
NETWORK TAB — SEE 4 PARALLEL CALLS as maxCount is 4

Implement using Promise

const throttlePromises = (promises, max) => {
return new Promise((resolve, reject) => {
try {
let counter = 0;
const poolResponses = [];

function executeChunk() {
const chunkPromises = promises.slice(counter, counter + max);
counter += max;

Promise.all(chunkPromises.map(p => p()))
.then(results => {
poolResponses.push(...results);

if (counter < promises.length) {
executeChunk();
} else {
resolve(poolResponses);
}
})
.catch(error => {
reject(error);
});
}

executeChunk();
} catch (err) {
reject(err);
}
});
};


// Example usage:
const promises = [
() => fetch('https://jsonplaceholder.typicode.com/todos/1'),
() => fetch('https://jsonplaceholder.typicode.com/todos/2'),
() => fetch('https://jsonplaceholder.typicode.com/todos/3'),
() => fetch('https://jsonplaceholder.typicode.com/todos/4'),
() => fetch('https://jsonplaceholder.typicode.com/todos/5'),
() => fetch('https://jsonplaceholder.typicode.com/todos/6'),
() => fetch('https://jsonplaceholder.typicode.com/todos/7'),
() => fetch('https://jsonplaceholder.typicode.com/todos/8'),
() => fetch('https://jsonplaceholder.typicode.com/todos/9'),
() => fetch('https://jsonplaceholder.typicode.com/todos/10'),
() => fetch('https://jsonplaceholder.typicode.com/todos/11'),
() => fetch('https://jsonplaceholder.typicode.com/todos/12'),
// Add more promise functions as needed
];
const maxConcurrentCalls = 4;
// Using throttlePromises
throttlePromises(promises, maxConcurrentCalls)
.then(results => {
console.log('All promises completed with results:', results);
})
.catch(error => {
console.error('Error:', error);
});

Implement using ASYNC/ AWAIT,

/**
* @param {() => Promise<any>} func
* @param {number} max
* @return {Promise}
*/
const throttlePromises = async (promises, max) => {
try {
let counter = 0
const poolResponses = []
while (counter <= promises.length) {
const chunkPromises = promises.slice(counter, counter + max)
counter += max
const results = await Promise.all(chunkPromises.map(p => p()))
poolResponses.push(...results)
}
return poolResponses
} catch (err) {
throw err
}
}

Challenge 1: Promise Constructor

What is the output of this code snippet?

console.log('start'); const promise1 = new Promise((resolve, reject) => {  
console.log(1)
}) ;
console.log('end');

Synchronized code blocks are always executed sequentially from top to bottom.

  • When we call new Promise(callback), the callback function will be executed immediately.

Output, So this code is to sequentially output start, 1, end.

Challenge 2: .then()

What is the output of this code snippet?

console.log('start'); const promise1 = new Promise((resolve, reject) => {  
console.log(1)
resolve(2)
})
promise1.then(res => {
console.log(res)
})
console.log('end');

Remember, the JavaScript engine always executes synchronous code first, then asynchronous code.

Output, So the output is start , 1 , end and 2 .

Challenge 3: resolve()

What is the output of this code snippet?

console.log('start'); const promise1 = new Promise((resolve, reject) => {  
console.log(1)
resolve(2)
console.log(3)
})
promise1.then(res => {
console.log(res)
})
console.log('end');

Remember, the resolve method does not interrupt the execution of the function. The code behind it will still continue to execute.

Output, So the output result is start , 1 , 3, end and 2 .

Challenge 4: resolve() isn’t called

console.log('start'); const promise1 = new Promise((resolve, reject) => {  
console.log(1)
})
promise1.then(res => {
console.log(2)
})
console.log('end');

The resolve method has never been called, so promise1 is always in the pending state. So promise1.then(…) has never been executed. 2 is not printed out in the console.

Output, So the result is start , 1 , end

Challenge 5: The One That’s There to Confuse You

What is the output of this code snippet?

console.log('start') const fn = () => (new Promise((resolve, reject) => {   
console.log(1);
resolve('success')
}))
console.log('middle') fn().then(res => {
console.log(res)
})

console.log('end')

This code deliberately adds a function to confuse challengers, and that is fn.

Output, So the result is start , middle, 1 , end and success.

Challenge 7: setTimeout vs Promise

What is the output of this code snippet?

console.log('start') setTimeout(() => {  
console.log('setTimeout')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')

This is a very difficult question. If you can answer this question correctly and explain the reason, then your understanding of asynchronous programming in JavaScript has reached an intermediate level.

In JavaScript EventLoop, there is also the concept of priority.

  • Tasks with higher priority are called microtasks. Includes: Promise, ObjectObserver, MutationObserver, process.nextTick, async/await .
  • Tasks with lower priority are called macrotasks. Includes: setTimeout , setInterval and XHR .

Output, So the result is start , end , resolve and setTimeout.

Challenge 8: Microtasks mix Macrotasks

const promise = new Promise((resolve, reject) => {  
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);

We just need to do three steps:

  1. Find the synchronization code.
  2. Find the microtask code
  3. Find the macrotask code

First, execute the synchronization code:

Output 1 , 2 and 4 .

Then execute microtask:

But here is a trap: Since the current promise is still in the pending state, the code in this will not be executed at present.

Then execute macrotask:

Then, with Event Loop, execute the microtask again:

Challenge 9: Prioritise Between Microtasks and Macrotasks

What is the output of this code snippet?

const timer1 = setTimeout(() => {  
console.log('timer1');
promise1 = Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)
const timer2 = setTimeout(() => { console.log('timer2')}, 0)

Some friends may think microtasks and macrotasks are executed like this:

  1. Execute all microtasks first
  2. Execute all macro tasks
  3. Execute all microtasks again
  4. cycle through

But the above statement is wrong.

The correct understanding is:

  1. Execute all microtasks first
  2. Execute one macrotask
  3. Execute all (newly added) microtasks again
  4. Execute next macrotask
  5. Cycle through

Challenge 10: A Typical Interview Question

Well, this is our final challenge. If you can correctly say the output of this code, then your understanding of Promise is already very strong. And the same type of interview questions won’t bother you in any way.

Remember what we learned earlier:

  1. Sync code
  2. All microtasks
  3. First macro task
  4. All newly added microtasks
  5. Next macro task

So:

  1. Execute all sync code:

2. Execute all microtasks

3. Execute the first macro task

4. Execute all newly added microtasks

5. Execute the next macro task

Conclusion

For all similar questions, you just need to remember three rules:

  1. The JavaScript engine always executes synchronous code first, then asynchronous code.
  2. Microtasks have a higher priority than macrotasks.
  3. Microtasks can cut in lines in Event Loop.

LIST OF INTERVIEW QUESTIONS:

What is a Promise in JavaScript?

  • Explain the concept and purpose of Promises.

What are the three states of a Promise?

  • Describe the pending, fulfilled, and rejected states.

How do you create a Promise in JavaScript?

  • Provide examples of creating a Promise using the Promise constructor.

Explain the role of the resolve and reject functions in a Promise.

  • What happens when each of them is called?

How do you handle the result of a fulfilled Promise?

  • Demonstrate the use of the .then() method.

How do you handle errors in a Promise?

  • Discuss the use of the .catch() method.

What is Promise chaining?

  • Show an example of chaining multiple Promises.

Explain the purpose of the .finally() method in a Promise.

  • How is it different from .then() and .catch()?

What is Promise.all()?

  • Describe its use and provide an example.

What is Promise.race()?

  • Explain the concept and provide an example.

What is the difference between Promises and callbacks?

  • Discuss the advantages of using Promises over callbacks.

How do you convert callback-based functions to use Promises?

  • Discuss the pattern of using Promises to handle asynchronous operations instead of callbacks.

Let’s take an example of a callback-based function:

function fetchDataFromAPI(callback) {
// Simulating an asynchronous operation (e.g., fetching data from an API)
setTimeout(() => {
const data = { message: 'Data received from API' };
callback(null, data);
}, 1000);
}

// Using the callback-based function
fetchDataFromAPI((error, result) => {
if (error) {
console.error('Error:', error);
} else {
console.log('Result:', result);
}
});

Now, let’s convert this callback-based function to use Promises:


function fetchDataFromAPI() {
return new Promise((resolve, reject) => {
// Simulating an asynchronous operation (e.g., fetching data from an API)
setTimeout(() => {
const data = { message: 'Data received from API' };
resolve(data); // Resolve the Promise with the data
// If an error occurs, use reject(error) instead of resolve(data)
}, 1000);
});
}

// Using the Promise-based function
fetchDataFromAPI()
.then(result => {
console.log('Result:', result);
})
.catch(error => {
console.error('Error:', error);
});

What is async/await and how does it relate to Promises?

  • Explain the purpose of async functions and the await keyword.

How can you handle multiple asynchronous operations sequentially using Promises?

  • Show an example of chaining Promises to execute operations one after the other.

What is the event loop in JavaScript, and how does it relate to Promises?

  • Discuss the event loop and how Promises are used to handle asynchronous tasks in the event-driven environment of JavaScript.

Reference,

Awesome article I found on Promises, curated the articles into one … This is very important article to understand the promise polyfills and challenges which is asked in interviews.

- https://towardsdev.com/6-promise-problems-that-front-end-engineers-should-master-for-interviews-8281848d7721
- https://betterprogramming.pub/10-javascript-promise-challenges-before-you-start-an-interview-c9af8d4144ec

--

--