Just an interview question π
The interview question: Re-implement Javascript Promise from scratch. The implementation should support chaining (asynchronously, of course).
Promise API
It has been too long since I started using
async
/await
. I almost forgot these Promise APIs.
Letβs revisit the basic Javascript Promise APIs
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
// try catch each one separately
myPromise
.then(handleResolvedA, handleRejectedA)
.then(handleResolvedB, handleRejectedB)
.then(handleResolvedC, handleRejectedC);
// or catch the first one if any
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);
A very basic implementation
Here is a trivial implementation with just resolve
and reject
support
- A
MyPromise
class - A constructor that provides
resolve
andreject
functions for the executor
type ResolveFn = (res: any) => void;
type RejectFn = (err: any) => void;
type PromiseExecuteFn = (resolve: ResolveFn, reject: RejectFn) => void;
export default class MyPromise {
constructor(execute: PromiseExecuteFn) {
const resolve: ResolveFn = (res) => {
console.log(`Resolved ${res}`);
};
const reject: RejectFn = (err) => {
console.log(`Rejected ${err}`);
};
try {
execute(resolve, reject);
} catch (err) {
reject(err);
}
}
}
const resolvePromise = new MyPromise((resolve, reject) => {
resolve('success');
});
const rejectPromise = new MyPromise((resolve, reject) => {
reject('error');
});
Adding then support
then
support for Promise is not difficult but a bit tricky since you have to handle async tasks.
- Implement the
then
function withhandleResolved
andhandleRejected
params - Defer the execution of those 2 functions until the promise has been resolved or rejected
- One thing to notice is that each promise can only be resolved or rejected once so you will need to store its status somewhere.
Letβs look at the updated version
type ResolveFn = (res: any) => void;
type RejectFn = (err: any) => void;
type PromiseExecuteFn = (resolve: ResolveFn, reject: RejectFn) => void;
type HandleResolvedFn = (res: any) => any;
type HandleRejectedFn = (err: any) => any;
export default class MyPromise {
private status: 'init' | 'resolved' | 'rejected' = 'init';
private handleResolved: HandleResolvedFn;
private handleRejected: HandleRejectedFn;
constructor(execute: PromiseExecuteFn) {
const resolve: ResolveFn = (res) => {
if (this.status !== 'init') return; // only resolve/reject once
this.status = 'resolved';
this.handleResolved && this.handleResolved(res);
};
const reject: RejectFn = (err) => {
if (this.status !== 'init') return; // only resolve/reject once
this.status = 'rejected';
this.handleRejected && this.handleRejected(err);
};
try {
execute(resolve, reject);
} catch (err) {
reject(err);
}
}
then(handleResolved: HandleResolvedFn, handleRejected?: HandleRejectedFn) {
// don't call these handleResolved and handleRejected function here
// defer them until the promise is resolved or rejected
this.handleResolved = handleResolved;
this.handleRejected = handleRejected;
}
}
const resolvePromise = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('success'), 300);
}).then((value) => console.log(value));
const rejectPromise = new MyPromise((resolve, reject) => {
setTimeout(() => reject('error'), 300);
}).then(
(value) => {},
(err) => console.log(err)
);
Chaining support
Chaining support is probably the most complicated part. π
In order to make the then
function chainable, you need to return
another Promise, which wraps the handleResolved
and handleRejected
functions.
- The
then
function will return another Promise (the inner Promise) - The inner Promise wraps the
handleResolved
function and resolves itself when the outer Promise has been resolved - The inner Promise wraps the
handleRejected
function and rejects itself when the outer Promise has been rejected
Here is the first implementation of the then
function
then(handleResolved: HandleResolvedFn, handleRejected?: HandleRejectedFn) {
return new MyPromise((resolve, reject) => {
// wrap the handleResolved function and resolve this one
this.handleResolved = (outerRes) => {
try {
const innerRes = handleResolved(outerRes);
resolve(innerRes);
} catch (e) {
reject(e);
}
};
// wrap the handleRejected function and reject this one
this.handleRejected = (outerErr) => {
try {
// you need this if/else so in case the then doesn't provide the
// handleRejected function, the error will be cascaded downstream
if (handleRejected) {
const innerErr = handleRejected(outerErr);
reject(innerErr);
} else {
reject(outerErr);
}
} catch (e) {
reject(e);
}
};
});
}
Here is a basic test case
import MyPromise from './promise';
new MyPromise((resolve, reject) => {
setTimeout(() => resolve('success 1'), 300);
})
.then((value) => {
console.log(value);
return 'sucesss 2';
})
.then((value) => {
console.log(value);
return 'success 3';
})
.then((value) => {
console.log(value);
});
// This will print
// success 1
// success 2
// success 3
Other test cases, see below.
Promise of Promise of Promise
The handleResolved
and handleRejected
functions can also return a Promise, sghhhhh. π₯²
Ok, simply add a check if handleResolved
returns a Promise and let that Promise resolve/reject itself.
this.handleResolved = (outerRes) => {
try {
const innerRes = handleResolved(outerRes);
if (innerRes instanceof MyPromise) {
// Promise of Promise of Promise
innerRes.then(resolve, reject);
} else {
resolve(innerRes);
}
} catch (e) {
reject(e);
}
};
and here is how to test
new MyPromise((resolve, reject) => {
setTimeout(() => resolve('success 1'), 300);
})
.then((value) => {
console.log(`resolve ${value}`);
return new MyPromise((resolve, reject) => {
setTimeout(() => resolve('success 2'), 300);
});
})
.then((value) => {
console.log(`resolve ${value}`);
});
// Wait 300ms and then print
// "resolve success 1"
// Wait 300ms and then print
// "resolve success 2"
Full version
You can find the full implementation here
You can also find all the test cases here
- Resolve immediately
- Basic then support
- Basic chaining
- Chaining with Reject 1
- Chaining with Reject 2
- Throw error
- Chaining with Promise
Or simply clone the full repo here