skip to content
Emmet Delaney

I Promise, I’ll be right back

/ 5 min read

Promises: a great headache in JavaScript land. Marking functions async all the way to the top. Callback hell. Knowing when to use await. How to catch errors. Race conditions. These are all issues you will face when using Promises in JavaScript, but it doesn’t have to be this way. Promises are a very important primitive and are extremely powerful when used correctly. In this post, I hope to break them down and make them slightly less intimidating. Let’s face it, no one likes broken promises.

First, what are Promises at their core?

A Promise is basically how it sounds. You hand a request off to the JS engine. In this example, we’re using a fetch request. This request runs on a separate thread, allowing the main thread to keep going. The JavaScript engine promises to return to it later, once the promise is settled. For us, that’s when the server answers the HTTP request.

💡 Side note - the main thread is like the main worker in a factory. It handles most of the tasks, but sometimes it needs help with bigger jobs. Promises are like hiring temporary workers to take on those bigger tasks, so the main worker can keep doing its own thing and everything runs smoothly.

How are they useful

Promises are useful as they allow work to continue on the main thread while allowing other tasks to proceed simultaneously. Think of it like a queue for a checkout in a shop.

  • At the top of the queue is a customer with a large basket of items. They are taking a long time to complete their checkout, and there are other people waiting in the queue; the line is starting to get long.
  • So, instead of letting all the other people wait, another cashier comes along and opens up a new lane for the “slow” customer.
  • This allows the “slow” customer to check out all her items in parallel with all the customers that came behind.
  • Once the “slow” customer is finished, the cashier on that till informs the cashier on the main till that she is finished serving the “slow” customer, and that everything went successfully, and they are closing that till for now.
  • In addition to all this, the customers in the main queue have now been mostly processed, and nothing was slowed down. The manager of the store is happy because productivity was kept up on the main till, and no one is complaining about a slow experience. Everyone is happy. Hooray!

Now, if we contrast this to the experience that would have happened if there was not a way of opening a second till (a promise), all the customers behind the “slow” customer (network request) would have been waiting quite a while for the “slow” customer to finish. From an outsider’s perspective, the manager (end user) would have been unhappy too; everything appears to be moving slowly and productivity is down. It was a poor customer experience.

This is the beauty of using promises: it allows us to move tasks off the main thread. This way, we can move on with other tasks while waiting for the promise to resolve. From the end user’s perspective, it will appear faster as rendering of a page happens on the main thread, and if it is blocked, it will give a poor user experience.

Promises in action

Below we have the basics of a promise. What do you think will log to the console first?

(Hint - it’s the last line.)

const networkRequest = fetch('https://api.example.com/data'); // Returns a promise
networkRequest
.then(response => {
console.log("Promise fulfilled!", response); // Handle the successful response
})
.catch(error => {
console.error("Promise rejected!", error); // Handle errors
});
console.log("This runs before the promise resolves!");

Explanation:

  1. fetch returns a promise immediately.
  2. .then() registers a callback to run when the promise is fulfilled.
  3. .catch() registers a callback to run if the promise is rejected.
  4. The final console.log demonstrates that the promise code runs asynchronously.

The reason that the second line logs out first is that once the promise is added the event loop, and the event loop moves on. The second console.log is then called. Once this has completed the promised will signal that it is resolved and the event loop will then process it. This is something to watch out for when using promises so to avoid race conditions. Read more about the event loop and race conditions.

Async/Await (A Simpler Way)

Now that we know that promises can affect the order of how things are called, how can we avoid this? Enter the await keyword. This tells the JS engine that we need to wait for the promise to resolve or reject before moving on with the code.

async function fetchData() {
try {
const response = await fetch('https://api.example.com/unknown_data');
console.log("Data fetched:", response);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
  • async makes the function return a promise.
  • await waits for the function until the promise resolves, then assigns the result.
  • try...catch handles errors more cleanly.

Important Note: Error handling is crucial with promises! Always use .catch() or try...catch to prevent unhandled rejections. If using the await keyword it is important to wrap your code in a try...catch, this way you can react to unhandled errors and stop your script from crashing!

Conclusion

Promises are a very powerful primitive in JS land and they can be very useful in a lot of situations. However used incorrectly they can cause you more headaches that you need. Hopefully the above can help give you a basic understanding of them and how to use them better. For more information on them or any other javascript concepts, I’d highly recommend checkout the MDN Docs

Takeaways

  • Promises are great for freeing up the main thread when doing some “slow” work.
  • await keyword can help cleanup your code while using them.
  • try...catch and .catch() are important to use to handle any errors.

Resources

Thanks for reading, happy coding!