Promises have become a crucial part of JavaScript, enabling cleaner asynchronous code and reducing the notorious “callback hell.” Despite their importance, understanding how Promises work under the hood can be challenging. In this blog post, we’ll build a Promise polyfill to gain a deeper understanding of Promises and answer related interview questions that often arise in JavaScript interviews.
What is a Promise?
A JavaScript Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. A Promise has three states:
- Pending: The initial state, neither fulfilled nor rejected.
- Fulfilled: The operation completed successfully.
- Rejected: The operation failed.
Here’s an example of how a Promise is typically used:
const asyncTask = new Promise((resolve, reject) => {
setTimeout(() => resolve("Task completed"), 1000);
});
asyncTask.then(result => console.log(result)).catch(error => console.error(error));
Interview Question: Implementing a Promise Polyfill
Implementing a Promise polyfill is a common interview question to test your understanding of Promises. We will create a simple polyfill to replicate the core functionality of a Promise in JavaScript.
Step 1: Creating the Basic Structure
Let’s start by creating a basic MyPromise
class that accepts an executor function. This executor function takes two arguments: resolve
and reject
, which are used to control the state of the Promise.
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.handlers = [];
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
resolve(value) {
this.updateState('fulfilled', value);
}
reject(error) {
this.updateState('rejected', error);
}
updateState(state, value) {
if (this.state !== 'pending') return;
this.state = state;
this.value = value;
this.handlers.forEach(this.handle.bind(this));
}
handle(handler) {
if (this.state === 'fulfilled') {
handler.onFulfilled(this.value);
} else if (this.state === 'rejected') {
handler.onRejected(this.value);
} else {
this.handlers.push(handler);
}
}
}
Explanation:
- State Management: The
MyPromise
constructor initializes the promise in apending
state and sets up an empty arrayhandlers
to storethen
handlers. - Resolve and Reject: These methods call
updateState
to change the state and value of the promise. Once a promise is no longer pending, it won’t change state. - Handler Queue: To handle asynchronous behavior, we store handlers in a queue and execute them when the promise fulfills or rejects.
Step 2: Adding then
and catch
Methods
The then
method is used to handle fulfilled or rejected values, while catch
is a shorthand for handling errors.
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.handle({
onFulfilled: value => {
try {
resolve(onFulfilled ? onFulfilled(value) : value);
} catch (error) {
reject(error);
}
},
onRejected: error => {
try {
reject(onRejected ? onRejected(error) : error);
} catch (err) {
reject(err);
}
}
});
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
Explanation:
- Then Method: Creates a new promise, which enables chaining by returning a new
MyPromise
instance. - Catch Method: Provides error handling by invoking
then
with a null first argument, which directly catches errors.
Step 3: Adding finally
Support
The finally
method is executed regardless of the promise outcome, making it useful for cleanup operations.
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
}
Explanation:
- Finally: Ensures that the callback runs for both fulfilled and rejected outcomes, returning the original value or throwing the original error.
Step 4: Adding Static resolve
and reject
Methods
To make our polyfill more complete, let’s add static resolve
and reject
methods, which create a fulfilled or rejected promise immediately.
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(error) {
return new MyPromise((_, reject) => reject(error));
}
Complete Implementation of the Promise Polyfill
Here’s the full MyPromise
class code:
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.handlers = [];
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
resolve(value) {
this.updateState('fulfilled', value);
}
reject(error) {
this.updateState('rejected', error);
}
updateState(state, value) {
if (this.state !== 'pending') return;
this.state = state;
this.value = value;
this.handlers.forEach(this.handle.bind(this));
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.handle({
onFulfilled: value => {
try {
resolve(onFulfilled ? onFulfilled(value) : value);
} catch (error) {
reject(error);
}
},
onRejected: error => {
try {
reject(onRejected ? onRejected(error) : error);
} catch (err) {
reject(err);
}
}
});
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
}
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(error) {
return new MyPromise((_, reject) => reject(error));
}
handle(handler) {
if (this.state === 'fulfilled') {
handler.onFulfilled(this.value);
} else if (this.state === 'rejected') {
handler.onRejected(this.value);
} else {
this.handlers.push(handler);
}
}
}
Related Interview Questions
- What is the purpose of the
finally
method in Promises?
- Answer: The
finally
method allows you to run cleanup actions after a Promise completes, regardless of its outcome. It is helpful for code that must run irrespective of the promise’s state, such as closing a database connection.
- Explain
Promise.all
,Promise.race
,Promise.allSettled
, andPromise.any
.
- Answer: These are utility methods that handle multiple promises at once:
Promise.all
: Resolves when all promises resolve; if any promise rejects, it rejects.Promise.race
: Resolves or rejects as soon as the first promise settles.Promise.allSettled
: Waits for all promises to settle, regardless of outcome.Promise.any
: Resolves as soon as the first promise fulfills; if all reject, it rejects.
- How can you cancel a Promise?
- Answer: JavaScript doesn’t natively support promise cancellation, but you can mimic it using techniques like
AbortController
or rejecting the promise conditionally. Using async generators or libraries like RxJS can also provide cancellable async behavior.
- Why are Promises beneficial over traditional callbacks?
- Answer: Promises provide cleaner syntax, error propagation, and better support for chaining asynchronous operations, reducing callback nesting and improving readability.
- How would you implement a
Promise.all
polyfill?
- Answer: A
Promise.all
polyfill iterates through an array of promises, maintaining a count of completed promises. Once all resolve, it fulfills; if any reject, it rejects.
Additional Essential JavaScript Interview Questions on Various Topics
- Master JavaScript Modules vs. CommonJS: The Ultimate 2024 Guide
- Ultimate Guide to Mastering JavaScript Symbols, Scope, and Immutability in 2024
- Mastering SOLID Principles in JavaScript: A Guide with Code Examples 2024
- Mastering Design Patterns for Frontend Developers: A Comprehensive Guide
- Understanding JavaScript Closures: A Comprehensive Guide
- JavaScript Event Loop: A Deep Dive with Examples 2024
- Web Workers: Empowering Frontend Development with This Ultimate Guide 2024
- Service Workers: Enhancing JavaScript Performance with This Definitive Guide 2024
- Arrow Functions vs. Normal Functions in JavaScript 2024
- Understanding call, bind, and apply in JavaScript 2024
- Web Security Essentials: Protecting Against CSRF, XSS, and Other Threats 2024
- Frontend Security: Best Practices for Authentication and Authorization 2024
- localStorage vs sessionStorage: The Ultimate Guide to Mastering Web Storage in JavaScript for 2024
- Variable Scopes Demystified: The Ultimate Beginner’s Guide to JavaScript 2024
- Javascript
React Js Interview questions:
- Mastering React Server-Side Rendering (SSR): A Deep Dive into SSR, CSR, and SSG
- Code Splitting and Lazy Loading in React: Boost Performance in Large Applications
- Higher-Order Components (HOC): Are They Still Relevant in 2024?
Mastering the useReducer Hook in React 2024: The Ultimate Guide for Advanced State Management - How Does React’s Context API Work? When Would You Use It Instead of a State Management Library Like Redux?
- Mastering React Hooks: The Ultimate 2024 Guide with Detailed Examples
- Virtual DOM: How Does React’s Reconciliation Algorithm Work?
- useEffect Hook in React: In-Depth Explanation and Performance Optimization
Top Javascript Books to Read
- You Don`t Know JS: 6 Volume Set (Greyscale Indian Edition) Paperback – 1 January 2017– by Kyle Simpson (Author)
- JavaScript: The Definitive Guide: Master the World’s Most-Used Programming Language, 7th Edition (Greyscale Indian Edition) [Paperback] David Flanagan – by David Flanagan | 11 July 2020
- JavaScript and HTML5 Now Kindle Edition– by Kyle Simpson
- Coding with Javascript for Dummies– by Chris Minnick and Eva Holland | 1 January 2015
- JavaScript from Beginner to Professional: Learn JavaScript quickly by building fun, interactive, and dynamic web apps, games, and pages-by Laurence Lars Svekis, Maaike Van Putten, et al. | 15 December 2021
- Head First JavaScript Programming: A Brain-Friendly Guide [Paperback] Robson, Elisabeth and Freeman, Eric– by Elisabeth Robson and Eric Freeman | 1 January 2014
Conclusion
Implementing a Promise polyfill is an excellent exercise to understand the Promise pattern in-depth. In this blog, we walked through building a Promise polyfill from scratch, covering essential methods like then
, catch
, finally
, and static methods. This knowledge is highly useful for JavaScript developers aiming to gain mastery over asynchronous programming and is commonly asked in technical interviews.