Demystifying Javascript Promises

Javascript promises can be one of the more obscure concepts to understand for beginner developers as well as some mid-level developers.

According to the official Javascript language spec, a promise is:

…a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action’s eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.”

Still confused? Put a different way – A javascript promise is a plain old javascript object that represents a future value so the rest of the code can still be carried out in a synchronous manner. Javascript is non-blocking – this means that it will not wait for a task to complete before moving on to the next line. Instead of waiting for the asynchronus task to complete, the promise object acts as a “place holder” to represent the future value.

The promise object contains several crucial key/value pairs:

Value – Immediately is undefined and upon successful fulfillment of the operation, value will contain the resulting data that has been returned.

OnFulfillment – Initially, an empty array that will serve as a “repository” of methods to invoke if and when the promise has resolved successfully.

OnRejection – Initially, an empty array that will again serve as a “repository” of methods to invoke if and when the promise has not resolved successfully.

So in summary, the promise object will immediately look like this:

{ value: undefined, onFulfillment: [], onRejection: [] }

Where does .then() and .catch() come into play?

One common misconception is that .then() and .catch() are some kind of magical methods that will allow the code within the parenthesis to somehow to run sometime in the future. Though technically true in that they do allow code to be run in the future, the way both methods work under the hood are somewhat simplistic in nature so it seems like magic to us.

.then() and .catch() are essentially pushing the function that we define within the parenthesis into the respective empty arrays on the newly created promise object this way they can be invoked once the time comes. (More on this in a bit)

So if we write myPromise.then(response => console.log(response))

We are more or less doing the following:

const futureFunction = (response) => console.log(response)
myPromise.onFulfillment.push(futureFunction)

And if we write myPromise.catch(response => console.log(response))

We are more or less doing the following:

const futureFunction = (response) => console.log(response)
myPromise.onRejection.push(futureFunction)

Once the promise has resolved the value is the inherently passed in to each provided function – in the above example we’re calling the value “response”. A lot of times in production code when making a fetch request to an API you will see a conditional checking if response.ok – this means that we’re checking to see if the HTTP status code we received back from the server is 200 (technically 2xx).

When do the functions we provide to .then() and .catch() actually run?

This can probably be its own blog post on its own given how in depth you can get with this topic. The functions we provide to .then() and .catch() don’t just run whenever we finally get a response back from whatever task we initially started. Remember: Javascript is non-blocking so once it’s done with a line of code that’s it – it’s not going to suddenly go back up several lines in the execution context and re-run lines of code because we got a response back from an API call.

Enter the concept of the call stack, the event loop, and the microtask queue.

While we are executing our Javascript code we’re constantly pushing and popping things from the call stack – We will start in the global execution context and then as we enter functions we find ourselves diving deeper and deeper into the execution contexts of those same functions and therefore pushing more and more onto the call stack. Remember: The call stack is like a stack of dishes in your sink – When you go to wash the dishes you’re going to take the dish that is at the top of the stack first and start washing it. This is know as Last In First Out, or LIFO for short. As we finish running those functions we pop them off of the call stack… And finally, when we’re done running all of the code in the Javascript file (the global execution context) we pop global off of the call stack as well and it’s finally empty – and then the magic can start.

The event loop essentially keeps checking to see if the call stack is empty. Once the call stack is indeed empty it will then allow any finished asynchronous tasks that have finished to then run – When a promise is finally resolved or rejected the contents of either the onFullfillment or onRejection arrays are then moved from the microtask queue onto the call stack via the event loop to finally be executed.