The thing I love most about programming is the aha! moment when you start to fully understand a concept. Even though it might take a long time and no small amount of effort to get there, it sure is worth it.
I am of an opinion that the most effective way to assess (and help improve) our degree of comprehension of a given subject is to try and apply the knowledge to the real world. Not only does this let us identify and ultimately address our weaknesses, but it can also shed some light on the way things work. Simple trial and error approach may often reveal the elusive details that hindered our understanding of the matter.
With that in mind, I firmly believe that learning how to implement promises was one of the most important moments in my programming journey - it has given me invaluable insight into how asynchronous code works and has made me a better programmer overall.
TypeScript is going to come in handy, too.
Given that we are going to be working on the skills of implementation here, I am going to assume you have some basic understanding of what promises are and and a vague sense of how they work. If you don’t, here is a great place to start.
Now that we have that out of the way, go ahead and clone the repository and let’s get started.
The core of a promise
As you know, a promise is an object with the following properties:
A method that attaches a handler to our promise. It returns a new promise with the value from the previous one mapped by one of the handler’s methods.
An array of handlers attached by then. A handler is an object containing two methods onSuccess and onFail, both of which are passed as arguments to then(onSuccess, onFail).
A promise can be in one of three states: resolved, rejected or pending.
Resolved means that either everything went smoothly and we received our value or we caught and handled the error.
Rejected means that either we rejected the promise or an error was thrown and we didn’t catch it.
Pending means that neither the resolve nor the reject method has been called yet and we are still waiting for the value.
The term “the promise is settled”, means that the promise is either resolved or rejected.
A value that we have either resolved or rejected.
Once the value is set, there is no way of changing it.
According to the TDD approach we want to write our tests before the actual code comes along, so let’s do just that.
Here are the tests for our core:
Running our tests
I highly recommend using the Jest extension for Visual Studio Code. It runs our tests in the background for us and shows us the result right there between the lines of our code as green and red dots for passed and failed tests respectively.
To see the result open the “Output” console and choose the “Jest” tab.
The result of the tests
We can also run our tests by executing the following command:
npm run test
Regardless of how we run the tests, we can see that all of them come back negative.
Let’s change that.
Implementing the Promise core
Our constructor takes a callback as a parameter.
We call this callback with this.resolve and this.reject as arguments.
Note that normally we would have bound this.resolve and this.reject to this, but here we have used the class arrow method instead.
Now we have to set the result. Please remember that we must handle the result correctly, which means that, should it return a promise, we must resolve it first.
First, we check if the state is not pending - if it is then the promise is already settled and we can’t assign any new value to it.
Then we need to check if a value is a thenable. To put it simply, a thenable is an object with then as a method.
By convention, a thenable should behave like a promise, so in order to get the result, we will call then and pass as arguments this.resolve and this.reject.
Once the thenable settles it will call one of our methods and give us the expected non-promise value.
So now we have to check if an object is a thenable.
It is important to realize that our promise will never be synchronous, even if the code inside the callback is.
We are going to delay the execution until the next iteration of the event loop by using setTimeout.
Now the only thing left to do is to set our value and status and then execute the registered handlers.
Again, make sure the state is not pending.
The state of the promise dictates which function we are going to use.
If it’s resolved, we should execute onSuccess, otherwise - onFail.
Let’s now clear our array of handlers just to be safe and not to execute anything accidentaly in the future. A handler can be attached and executed later anyways.
And that’s what we must discuss next: a way to attach our handler.
It really is as simple as it seems. We just add a handler to our handlers array and execute it. That’s it.
Now, to put it all together we need to implement the then method.
In then we return a promise and in the callback we attach a handler that is then used to wait for the current promise to be settled.
When that happens either handler’s onSuccess or onFail will be executed and we will proceed accordingly.
One thing to remember here is that neither of the handlers passed to then is required. It is important, however, we don’t try to execute something that might be undefined.
Also, in onFail when the handler is passed we actually resolve the returned promise, because the error has been handled.
Catch is actually just an abstraction over the then method.
Finally is also just an abstraction over doing then(finallyCb, finallyCb), because it doesn’t really care about the result of the promise.
Actually, it also preserves the result of the previous promise and returns it. So whatever is being returned by the finallyCb doesn’t really matter.
It will just return a string
Having implemented the core of our promises we can now implement some of the previously mentioned Bluebird methods, which will make operating on promises easier for us.
I believe the implementation is pretty straightforward.
Starting at collection.length we count down with each tryResolve until we get to 0, which means that every item of the collection has been resolved. We then resolve the newly created collection.
We simply wait for the first value to resolve and return it in a promise.
We iterate over keys of the passed object, resolving every value. We then assign the values to the new object and resolve a promise with it.
By using setTimeout we simply delay the execution of the resolve function by the given number of milliseconds.
This one is a bit tricky.
If the setTimeout executes faster than then in our promise, it will reject the promise with our special error.
We apply to the function all the passed arguments, plus - as the last one - we give the error-first callback.
We iterate over the keys of the object and promisify its methods and add to each name of the method word Async.
Presented here were but a few amongst all of the Bluebird API methods, so I strongly encourage you to explore, play around with and try implementing the rest of them.
It might seem hard at first but don’t get discouraged - it would be worthless, were it easy.
Thank you very much for reading! I hope you found this article informative and that it helped you get a grasp of the concept of promises and that from now on you will feel more comfortable using them or simply writing asynchronous code.