A photograph of the author, Alex Kharouk

Alex Kharouk

Asynchronous JavaScript: Promises

Asynchronous means we don't have the data, yet.

## What order does this log out?

First try figuring it out yourself, and then place it in the browser console.

setTimeout(() => {
console.log('1')
}, 0)
setTimeout(() => {
console.log('2')
}, 10)
Promise.resolve('3').then((data) => {
console.log(data)
})
console.log('4')

If you didn't, no worries. This is all due to how the JavaScript engine works when dealing with Web APIs1 and the event loop2. When the JavaScript engine sees something like setTimeout, it sends it over to the Web API to handle the it, then continues with the rest of the code, meeting the next setTimeout (or Promise in our case).

Finally, it runs the console.log('4') and the engine stops running, so to speak. However, we still have the Web Api's setTimeout functions that get returned, after we have already finished running the program. That's why we see the undefined (meaning the program has stopped running and returns nothing), and then we see 1 and 2.

So what's happening? Why does the promise "resolve" before the script finishes, and not setTimeout (which should only 0 and 10 milliseconds to complete)? Well, I guess we need to understand what the JavaScript engine is doing, under the hood. I have a separate blog which you can see here. Back already? Let's continue onwards!

## Promises

A Promise is an object that may produce a single value some time in the future. It is its namesake. It is a promise that needs to be resolved, or is rejected.

A promise can be in one of three states: pending, fulfilled, or rejected. Before promises were introduced in ES6, developers would use callbacks. Callbacks are a way to handle asynchronous code, where you'd have an action execute, and then the callback was called for further execution. When dealing with callbacks, sometimes you'd have nested callbacks to the extreme, where functions will have callbacks, and they will have callbacks, and so on. Obviously, this isn't ideal for the developer to debug and understand. It isn't ideal, and can easily lead to bugs. This is the pain point that promises are designed to solve.

utils/headToShop.ts
headToShop('groceries', car, () => {
headToShop('clothes', run, () => {
headToShop('shoes', ski, () => {
headToShop('accessories', walk, () => {
console.log('Done shopping for the day!')
})
})
})
})
// When using promises, it becomes:
headToShop('groceries', car)
.then(() => headToShop('clothes', run))
.then(() => headToShop('shoes', ski))
.then(() => headToShop('accessories', walk))
.then(() => console.log('Done shopping for the day!'))

It's neat, organised, and you can see the code looking a bit like normal synchronous code, which helps DX. The funky thing here is .then() chaining. That's because headToShop returns to us a promise, rather than a value.

utils/promise.ts
const promise = new Promise((resolve, reject) => {
// Make async calls here (timeout, api, database)
let success = true // change to replicate rejection
if (success) {
// We got the data, let's resolve the promise
resolve('Success!')
} else {
// Something went wrong, let's reject the promise
reject('Failure!')
}
})
promise.then((result) => console.log({ result }))

When we run this code, we create a new Promise that's stored into promise. Since success is true, this promise will resolve, giving us a value of Success!. We get the value of promise by using .then(), which takes a callback function. The callback function is called with the value of the promise (stored in result), and we see it logged.

Running this in the browser, we would see:

> {result: 'Success!'}
> Promise {<fulfilled>: undefined}
> promise # calling the function
> Promise {<fulfilled>: 'Success!'}

The undefined is interesting because when we call .then(), that's passed a callback function to log the result. So that gets added to the callback queue, the promise is returned (albiet fulfilled but with an undefined value). It's fulfilled because it has a resolved value, so it knows it will be updated. That's why when we call promise again, we see the success value within the object.

### Promise.all()

What if you have multiple promises? You might need to make multiple calls to different endpoints, pooling data together. It might be cumbersome to try and resolve multiple promises separately. In my case, I was using dynamic imports4 to grab all of my .mdx blog posts for this site, and was trying to extract the metadata (which is just a JS object). I would map over the directory, and would have to await the file contents. This returned to me an array of promises! I had to figure out a way to resolve them all at once, rather than one at a time.

utils/posts.ts
export function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory)
return fileNames.map(async (fileName) => {
// Removes .md from the file names to get the id
const id = fileName.replace(markdownExtensionRegex, '')
const postModule = await import(`../pages/posts/${fileName}`)
return {
id,
...postModule.meta,
}
})
}
const posts = getAllPosts() // returns [Promise, Promise, Promise, ...]

So whenever I tried to loop over the posts, I would just be greeted with a Promise object. At first, I decided to just resolve the promise as I looped over it, but there's no need. That's why Promise.all() is useful. It takes an array of promises, and resolves them all at once. The promises resolve when the longest-waiting promise has finished resolving. So, even if one promise takes a millisecond, we won't receive the value until all of them have resolved.

utils/posts.ts
const posts = Promise.all(getAllPosts()).then((posts) => posts)

## Finally...

Sometimes, after we have a promise the resolves or rejects, we want to do a final step. Maybe we want to terminate a connection with a database, or we want to create send metrics to a monitoring service. The .finally() function can be chained at the end of a .then/.catch, and it will execute regardless of whether the promise resolves or rejects.

Promise.all(getAllPosts())
.then((posts) => {
console.log(posts)
})
.catch((error) => {
console.error(error)
})
.finally(() => {
closeConnectionToDatabase()
})

## Some Last Words

Whenever we call a function without adding a .then() chain, or a .catch() chain, we just get a promise back, with a status of pending. We won't have the value, unless we write the handling ourselves. And since, JavaScript is synchronous in nature, if you try logging the value of a promise, right after creating the promise, you'll get undefined. We want this behaviour, despite it feeling frustrating when you don't know why it's undefined. It's what keeps JavaScript running, whilst we are able to have side-effects that take a while, without blocking anything else.

We started with callbacks, and now we have promises. Life is great, but life can be improved (for JS devs). The .then() is sufficient, but maybe there's more. You might know what I'm thinking of, or you might've spotted some keywords in my example snippet of fetching data asynchronously. The keywords async/await are that next step. The step towards no chaining, and no callback hell.

That's where we're heading next. I hope you enjoyed this post. If you have any questions, please feel free to send me a message on twitter.