Implementing Javascript Promise from scratch
I always find it fascinating to recreate non-trivial tools that I use every day. Promises have been an integral part of our JS lives since ES6. Most async operations are now handled with Promises to avoid callback hell. They even got a syntactic sugar async await
to make them even more comfortable to work with. The inner implementation of the Promise class has always been interesting to me, so I decided to build a class with minimal Promise functionality.
I want this Future
class to be instantiated with the new
keyword like a Promise, be able to resolve it, and be infinitely thennable. The .then
method should also flatten internal Future
s just like returning a Promise from Promise.then flattens it. All of this should be done while being fully typed with typescript - this will make not only the usage easier, but also reasoning about the logic itself. I will not go into rejecting a Future or calling the .catch
method.
The Future class should have a signature that can be used like this.
new Future<number>(res => {
setTimeout(() => {
res(1);
}, 1000);
})
.then(val => val * 2)
.then(val => console.log(val));
Basically, the Future constructor gets a callback that gets called with the resolve
function as it's argument. When resolve is called, it should pass the value to the callback inside .then
. This example should log the value 2 after 1 second. We will worry about flattening internal Futures in Part 2.
First, let's create the constructor.
class Future<T> {
constructor(init: (resolve: (el: T) => void) => void) {
const resolve = (el: T) => {}
init(resolve)
}
}
While this won't work, it supports the signature. The el
value passed to resolve should be passed as an argument to the .then
method. Let's implemented that method. As the .then can be chained, it is simplest to recursively return a new Future from the .then method. The argument to the .then
method is a callback that takes parameter of type T
, modifies it (changes it to type R
), and returns a new Future that will resolve to type R
.
then<R>(modifier: (el: T) => R): Future<R> {
return new Future(resolve => {
})
}
But when can we call the modifier
callback? Only once we have the initial Future resolve. To have fine grained control over when to call the modifier
, we can create a function that will call it some time in the future, and save it in a private array in the Future class.
private thenResolvers: ((el: T) => any)[] = [];
Now the then
method can be changed like so.
then<R>(modifier: (el: T) => R): Future<R> {
return new Future(resolve => {
this.thenResolvers.push((el) => {
const result = modifier(el);
resolve(result);
})
})
}
Last, but not least, we can add a private .next
method to call as the initial resolver. It will iterate on the thenResolvers
array, and call each one with the initial resolved value. Our new constructor and the .next methods look like this:
constructor(init: (res: (el: T) => void) => void) {
const resolve = (el: T) => this.next(el);
init(resolve);
}
private next(val: T) {
this.thenResolvers.forEach(thenResolver => {
thenResolver(val);
})
}
Now our initial example will work as expected. There is a slight problem - this code won't work if our Future resolves syncronously, because the .then
method will not have time to attach itself first. To fix that we can wrap the resolve callback to the init function in a setTimeout of 0, like so:
constructor(init: (res: (el: T) => void) => void) {
const resolve = (el: T) => {
setTimeout(() => {
this.next(el);
}, 0);
}
init(resolve);
}
All the code for this article looks like this.
class Future<T> {
constructor(init: (res: (el: T) => void) => void) {
const resolve = (el: T) => {
setTimeout(() => {
this.next(el);
}, 0);
}
init(resolve);
}
private thenResolvers: ((el: T) => any)[] = [];
private next(val: T) {
this.thenResolvers.forEach(thenResolver => {
thenResolver(val);
})
}
then<R>(modifier: (el: T) => R): Future<R> {
return new Future(resolve => {
this.thenResolvers.push((el) => {
const result = modifier(el);
resolve(result);
})
})
}
}
new Future<number>(res => {
setTimeout(() => {
res(1);
}, 1000);
})
.then(val => val * 2)
.then(val => console.log(val));
I recommend playing around with this code and trying to augment the .then
method so that it flattens internal Futures. You can use this snippet to test if it funcitons correctly:
new Future<number>(resolve => {
setTimeout(() => {
resolve(1);
}, 1000)
})
.then(val => val * 2)
.then(() => {
return new Future(res => {
setTimeout(() => {
res(0)
}, 1000)
})
})
.then(console.log)
This code should log the value 0
after 1 second. Our current implementation logs a Future object.