Introduction
Promise
is a powerful tool in JavaScript that allows for asynchronous processing and handling of data. It can be a tricky concept to implement, especially during a FAANG interview. However, with the right understanding and approach, you can successfully implement your own Promise
.
In this article, we will go over how to implement a basic version of a promise during a FAANG interview. The standard for promise implementation is called A+, but it includes a huge amount of details, making it almost impossible to implement all of them during a one-hour coding interview. Therefore, we will focus on implementing a basic variation that should be enough to show the interviewer your solving skills.
Implementation
TL;DR see the final code at the end
First, let's define the template for our promise. Promise
is a JS class that contains two main functions then()
and catch()
, as well as two additional static functions resolve()
and reject()
.
class MyPromise {
constructor(executor) {
// your code here
}
then(onFulfilled, onRejected) {
// your code here
}
catch(onRejected) {
// your code here
}
static resolve(value) {
// your code here
}
static reject(value) {
// your code here
}
}
Now, let's take a look at how promises work and write a basic example of usage:
const promise = new MyPromise((resolve, reject) => {
resolve('Test value');
console.log('resolved');
});
console.log(promise.status); // should be 'resolved'
console.log(promise.value); // should be 'Test value'
We'll start by implementing the constructor. Promise
can have three different states: pending
, fullfiled
, rejected
. During its creation, we should set the status to pending and then call the function passed to the constructor with parameters onFullfil
and onReject
.
constructor(executor) {
this. Status = 'pending';
try {
executor. Call(this, this.onFullfil.bind(this), this.onReject.bind(this));
} catch(e) {
this.onReject.call(this, e);
}
}
onFullfil(value) {
// your code here
}
onReject (value) {
// your code here
}
Now, the executor is being called. However, after resolve
is called, the status of the promise remains unchanged. Let's fix it:
onFullfil(value) {
if (this.status !== 'pending') return;
this.status = 'fullfiled';
this.value = value;
}
onReject (value) {
if (this.status !== 'pending') return;
this.status = 'rejected';
this. Value = value;
}
Now that the promise has the right value and status, let's revise how chaining of .then()
works:
promise.then((val) => {
console.log('Then ', val);
return 'New value';
}).then((val) => {
console.log('Second then ', val);
});
In order to enable chaining, the then()
method should return a new instance of the promise. To accomplish this, we can utilize the new MyPromise
constructor.
Additionally, we should run the appropriate callback, either onFulfilled
or onRejected
, based on the current state of the initial promise. The returned value from this callback should then be set as the value of the newly created promise instance. This allows for a chain of promises to be created, with each promise's value being determined by the return value of its corresponding callback.
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const newPromiseResolve = () => {
try {
const res = onFulfilled(this.value);
resolve(res);
} catch(e) {
reject(e);
}
};
const newPromiseReject = () => {
try {
if (onRejected) {
const res = onRejected(this.value);
resolve(res);
} else {
reject(this.value);
}
} catch(e) {
reject(e);
}
};
if (this.status === 'fullfiled') {
newPromiseResolve();
}
if (this.status === 'rejected') {
newPromiseReject();
}
});
}
While the implementation of the then()
method appears to be functioning as intended, it's important to consider the scenario where the resolve function is called async
. In order to properly handle this case, we can write a test case to evaluate the behavior of the promise in this case.
const asyncedPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
console.log('Async resolve');
resolve('I am async value');
}, 1000);
});
asyncedPromise.then((val) => {
console.log('Asynced then', val);
});
When the then()
method is called on a promise that is still in the pending
state, our current implementation may not handle this scenario properly. To address this, we should save any callbacks passed to the then()
method in separate arrays for resolve
and reject
callbacks. This way, when the promise's state changes from pending
to either fulfilled
or rejected
, we can run the appropriate callbacks that were previously stored. To implement this, we can initialize two arrays, resolveCallbacks
and rejectCallbacks
.
this.resolveCallbacks = [];
this.rejectCallbacks = [];
this.tasks = [];
Then add one more condition for a pending
status in .then()
function. We need to save passed callbacks to our inner state.
if (this.status === 'pending') {
this.resolveCallbacks.push(newPromiseResolve);
this.rejectCallbacks.push(newPromiseReject);
}
Now we need to write function runTasks()
to run all saved callbacks and clean arrays after.
doTask () {
const task = this.tasks.shift();
if (task) {
task.call(this, this.value);
}
}
runTasks () {
while(this.tasks.length) {
this.doTask();
}
}
Let's call runTasks
function inside onFulfill
and onReject
:
onFullfil(value) {
if (this.status !== 'pending') return;
this.status = 'fullfiled';
this.value = value;
this.tasks.push(...this.resolveCallbacks);
this.resolveCallbacks = [];
this.runTasks();
}
onReject (value) {
if (this.status !== 'pending') return;
this.status = 'rejected';
this.value = value;
this.tasks.push(...this.rejectCallbacks);
this.rejectCallbacks = [];
this.runTasks();
}
Congratulations, the implementation of the then()
method now works asynchronously
. However, we must not forget an important consideration when chaining multiple then()
calls together. If a promise instance is returned within the onFulfilled
or onRejected
callback passed to the then()
method, it should be handled correctly by the next then()
in the chain. According to the Promise specification, the next then()
in the chain should wait for the returned promise to resolve before proceeding, and the final resolved value should be passed as the input to the next then()
rather than the promise instance.
Example:
promise.then(
(val) => new MyPromise((res) => res('Value from promise'))
).then(
(val) => console.log(val) // should print 'Value from promise' string, not promise instance
)
Let's check if res
is a promise instance or just a value.
if (res instanceof MyPromise) {
res.then(resolve, reject); // if it's a promise instance wait for value and resolve/reject
} else {
resolve(res); // otherwise just resolve value
}
Now comes the easiest part. Let's implement the static methods. We simply need to create a promise and instantly resolve
or reject
its value.
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(value) {
return new MyPromise((_resolve, reject) => reject(value));
}
And the method catch()
:
catch(onRejected) {
return this.then((value) => value, onRejected);
}
The bonus part
Let's verify that all callbacks are called in the correct order by providing an example:
// Test: constructor should be sync, then handlers should be async
// order should be: 0, 1, 3, 2
console.log(0);
new MyPromise((resolve, reject) => {
console.log(1);
resolve(1);
}).then(() => {
console.log(2)
});
console.log(3);
If we run this test we will see 0, 1, 2, 3
order. But it's expected to be 0, 1, 3, 2
.
It happens because callbacks inside .then()
must be called async. Let's wrap it with setTimeout
to make it async.
setTimeout(() => {
newPromiseResolve();
}, 0);
setTimeout(() => {
newPromiseReject();
}, 0);
Summary
In summary, implementing a basic variation of a Promise
class involves creating a class with a constructor, methods to resolve
and reject
the promise, and methods to handle the callbacks passed to the then()
and catch()
methods. Additionally, you need to make sure that callbacks passed to the then()
method are called asynchronously and in the correct order.
Final version of the code:
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.resolveCallbacks = [];
this.rejectCallbacks = [];
this.tasks = [];
try {
executor.call(this, this.onFullfil.bind(this), this.onReject.bind(this));
} catch(e) {
this.onReject.call(this, e);
}
}
doTask () {
const task = this.tasks.shift();
if (task) {
task.call(this, this.value);
}
}
runTasks () {
while(this.tasks.length) {
this.doTask();
}
}
onFullfil(value) {
if (this.status !== 'pending') return;
this.status = 'fullfiled';
this.value = value;
this.tasks.push(...this.resolveCallbacks);
this.resolveCallbacks = [];
this.runTasks();
}
onReject (value) {
if (this.status !== 'pending') return;
this.status = 'rejected';
this.value = value;
this.tasks.push(...this.rejectCallbacks);
this.rejectCallbacks = [];
this.runTasks();
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const newPromiseResolve = () => {
try {
const res = onFulfilled(this.value);
if (res instanceof MyPromise) {
res.then(resolve, reject);
} else {
resolve(res);
}
} catch(e) {
reject(e);
}
};
const newPromiseReject = () => {
try {
if (onRejected) {
const res = onRejected(this.value);
if (res instanceof MyPromise) {
res.then(resolve, reject);
} else {
resolve(res);
}
} else {
reject(this.value);
}
} catch(e) {
reject(e);
}
};
if (this.status === 'pending') {
this.resolveCallbacks.push(newPromiseResolve);
this.rejectCallbacks.push(newPromiseReject);
}
if (this.status === 'fullfiled') {
setTimeout(() => {
newPromiseResolve();
}, 0);
}
if (this.status === 'rejected') {
setTimeout(() => {
newPromiseReject();
}, 0);
}
});
}
catch(onRejected) {
return this.then((value) => value, onRejected);
}
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(value) {
return new MyPromise((_resolve, reject) => reject(value));
}
}
Top comments (0)