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
MyPromiseclass - A constructor that provides
resolveandrejectfunctions 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
thenfunction withhandleResolvedandhandleRejectedparams - 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
thenfunction will return another Promise (the inner Promise) - The inner Promise wraps the
handleResolvedfunction and resolves itself when the outer Promise has been resolved - The inner Promise wraps the
handleRejectedfunction 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