Promise Fundamentals (part 3)
This is the final part of a three-part blog series that explores hand-rolled promises. (Check out part 1 and part 2). This deep dive aims to understand promises by building promises from the ground up. In this final post, we will make some final adjustments to our promise, add support to finally, and make it compatible with the await keyword.
additional adjustments
While doing some testing with PromiseKeeper, I discovered a bug when I ran our the following promise code:
promise.then(res => {
console.log(`First then fired ${res}`);
throw new Error("yikes");
}).then(res => {
console.log(`Second then fired ${res}`);
}).catch(reason => {
console.log("This catch will never be called", reason);
});
In the example code above, catch further down the chain from an exception will never receive the reason and will never be called. Taking a closer look at our implementation, we can see that we never keep passing up the chain of rejection. The problem code is this block of PromiseKeeper:
} else if (this.state == 'rejected') {
const result = onRejected ? onRejected(this.value) : this.value;
resolve(result);
}
The problem is we never call "reject" when in a state of "rejected." If our handle callback, which is the wrapper inside our "then" method, is in a state rejected and we happen not to have an onRejected callback, we just set the result to the current value and resolve. Instead, we should call reject, which will pass our failed value until we find a "thenable" promise with an onRejected. This can either be an actual "catch" or a then with the second callback. The fix looks like the following:
const result = onRejected ? onRejected(this.value) : reject(this.value);
finally
Finally, we should implement "finally." Although not mentioned in the A+ specification, the finally method is a form of syntactic sugar that is built into the native promise. Finally is always called. The finally method is even called when a throw or a reject occurs within the promise chain. In many ways, finally is simply a wrapper around a call to then. For example:
finally(onFinally) {
return this.then(onFinally, onFinally);
}
However, onFinally doesn't receive any value. So, our implementation will look like the following:
finally(onFinally) {
return this.then(
(value) => {
onFinally && onFinally();
return value;
},
(reason) => {
onFinally && onFinally();
throw reason;
}
);
}
Our implementation of finally will return or re-throw whatever value but not pass the the value or reason into the onFinally function.
await/async
Because we have implemented a promising class that is then able to interoperate with the native await/async keywords, According to MDN docs, there were many implementations of promises before the native built-in promises. Since this is true, we can now use our PromiseKeeper class with await like a native promise.
(async() => {
let res = await (new PromiseKeeper((resolve, reject) => {
resolve('done');
}));
console.log(res);
})()
It's an interesting an nice side-effect of implementing a thenable class.
conclusion
We have implemented a class that is an actual promise in pure javascript. Anyone interested can download the PromiseKeeper code from this gist. This concludes our exploration into promises.