Understand the Javascript Promise

Jamie Uttariello
6 min readOct 8, 2019

What is a promise? How can we better understand a promise? How does the JS Engine actually process a Promise when it sees it in the wild?

This article may not be 100% accurate as to the specs of the JS Promise (though it is generally accurate), but it hopefully will finally make the concept of Promises click in people’s brains when otherwise it has not. It is also my attempt to pound this in to my own brain. By writing about what you think you know, you discover what you don’t know, and correcting your writing until you get it right makes the concept truly stick in your brain.

A Javascript Promise

A Promise is a built-in javascript object that, when created, gets passed a callback function, which returns a resolve() or reject() function. There are two main properties of the Promise object, state and value. The default ‘state’ is ‘pending’, the default ‘value’ is ‘undefined’.

If the resolve() function is returned the ‘state’ changes to ‘fulfilled’ and the ‘value’ changes to whatever was passed into the resolve() function.

If the reject() function is returned the ‘state’ changes to ‘failed’ and the ‘value’ changes to whatever was passed into the reject() function.

The JS Engine Process for Handling the Promise

When the JS Engine sees ‘new Promise(fn)’ it immediately goes to the top of the stack, creates the object, and executes its callback function. The callback function immediately executes and while it is waiting for its return value it gets moved to the JS Engine’s API Container.

It waits until either its resolve() or reject() function is returned. When either is returned it gets sent to the JS Engine’s Queue and when the stack is clear and the function is next in line in the Queue the Event Loop passes it to the top of the Stack.

There, it executes the resolve() or reject() function, and updates the Promise object properties. It returns the passed in value to either a .then() chained method if it was resolved, or a .catch() method if it was rejected.

This process of moving the callback from the stack, to the the api container, to the queue, and back to the stack, is the whole purpose of Promises. It makes javascript work asynchronously, because while the callback is waiting to return a value, the rest of the Stack in the JS Engine continues to execute. This is what they mean when they say JS is a non-blocking IO. This long running callback function now sitting in the API container will not ‘block’ the stack from continuing its execution while it waits to return its value.

.then() & .catch()

The .then or .catch method expects a callback function with the not-required-to-be-passed-in value. In other words, you can do promise.then(() => console.log(‘hello’)) or you can do promise.then(data => console.log(data)) where ‘data’ is the value of the returned promise object’s value property.

The .then() callback function returns another Promise with updated state and value properties that are again passed to a .then() or .catch() method. You can stop this process of passing promises in the chain at any time, or you can keep it going infinitely.

A beautiful aspect of the Promise in javascript is the .catch() method, which can be put at the end of the chain. If at any step in the chain an error occurs the .catch() method at the end of the chain will be called with the error message passed in and all other .then() methods will be ignored.

This keeps you from having to do things like: .then().catch().then().catch Instead, you can just do .then().then().catch().

All that said, this is the very basics of Promises and not super accurate. For instance, every .then() method can have two callback functions as arguments. The first one handles the resolve method and the second one handles the error. I believe this part is what confuses the heck out of people. However, to more fully understand Promises, how to handle errors, how to pass values and use .then(), read Eric Elliott’s great article on Promises.

A Stupid, Pure Promises Example

Our goal will be to create a properly formatted phrase starting with an array of words.

First, we create a function that we will use in our promise that takes in an array of words and returns a string of words. In the Promise we call the function, passing in the array of words and we return a resolve method with the newly formed string. If there were no words in the array, we run the reject() method with an error message.

Note: run this through Google console to see that promises really do exist! Every promise is an object that has a returned state and a value. That is the real purpose of this exercise, to see what the console outputs so you can see the actual Promise.

const connectWords = arr => arr.join(' ')const createPhrase = arr => {
return new Promise((resolve, reject) => {
if(typeof arr === 'undefined' || !Array.isArray(arr)) {
return reject('Must pass in array!')
}
if(arr.length === 0) return reject('No words passed in!') // run other checks, like if all items in array are strings const wordsString = connectWords(arr) return resolve(wordsString) })
}
createPhrase(['big', 'little', 'lies'])

This doesn’t return anything because the final call, createPhrase([‘big’, ‘little’, ‘lies’]) simply returns the called Promise, which is just an object floating around in space. It was not stored anywhere and we didn’t chain any .then() or .catch() methods to handle its resolved or rejected value.

Here is how to store the Promise, and also how to chain it…

const newPhrase = createPhrase(['big', 'little', 'lies'])newPhrase.then(phrase => console.log(phrase))

Now, we will have consoled the phrase and returned another promise with an undefined value because we did not return a value, we just console logged. We can end it here, or, we can create more functions to further process the phrase and chain some .then() methods until we have a properly formatted phrase.

const capFirstLetter = str => str[0].toUpperCase() + str.slice(1)newPhrase
.then(phrase => capFirstLetter(phrase))
.then(phrase => console.log(phrase))

We’re getting there. In the first .then() method we run a callback which returns a function that executes and returns a value of the phrase with the first letter capitalized. But we need punctation at the end, so…

const addPunctuation = str => str + '!'newPhrase
.then(phrase => capFirstLetter(phrase))
.then(phrase => addPunctuation(phrase))
.then(phrase => console.log(phrase))

Hopefully, this helps to understand how a Promise is returned and what the value is that is passed to the next .then() method.

The end result would look like this….

const connectWords = arr => arr.join(' ')
const capFirstLetter = str => str[0].toUpperCase() + str.slice(1)
const addPunctuation = str => str + '!'
const createPhrase = arr => new Promise((resolve, reject) => {
const wordsString = connectWords(arr)
if(wordsString.length > 0){
return resolve(wordsString)
} else {
return reject('No words passed in!')
}
})let newPhrase = createPhrase(['big', 'little', 'lies'])newPhrase
.then(phrase => capFirstLetter(phrase))
.then(phrase => addPunctuation(phrase))
.then(phrase => console.log(phrase))
.catch(err => console.log(err))

This is a stupid and unrealistic example. The real reason you would use a Promise is because whatever you are doing within the Promise may take a while and you don’t want to block the Stack from running while you wait.

Another reason to work with Promises is because if you have multiple functions or steps like in the example above, if any of those steps cause an error, the error will stop the chain and jump right into the .catch() method. As a result it makes the code much easier to read and understand.

A More Realistic Example Using the fetch() Library

fetch(url)
.then(dataFromUrl => console.log(dataFromUrl))
.catch(err => console.log(err))
ORfetch(url)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err))

The fetch function internally creates a new Promise, within it, fetches data from an http request and either returns a resolve() or reject() function based on if the http request succeeds or fails.

If it succeeds, the response is the value of the promise and is thus accessible to the .then() chained method. fetch has a built in function to turn received data into a proper json object, so that function runs and returns its value into the the next .then(). In this example we console.log the result. In the real world, we would probably store the data so that we can use it in the frontend.

If the request failed, the .catch() method would have been called and the error message would have been console logged.

--

--