The solution to callback purgatory is promises. Promises are the idea of an IOU for something that will happen in the future. When we request a timer, api to reply, microphone or camera to be granted access to, we can send a promise or an "I owe you" in it's place.
If you haven't already checked out my last article on Event Loops & Callbacks, I'd recommend at least watching the video in it to get a better understanding of the Callstack, Web API, and Callback Loop so you can follow some of the knowledge dropped here.
In one of my previous tutorials for the Face Detection & Censorship app we used async
await
to allow the user to accept the "camera enabled" warnings in the browser before running.
// Example Time
When you order a pizza, it's not instantly in your mouth, you get an order number that tells you that the pizza is being made and your pizza is being created for delivery at some time in the future. The idea is that you create the order immediately but the creation of the pizza isn't instant.
function makePizza() { const pizzaPromise = new Promise(function (resolve, reject) { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`?`); //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); return pizzaPromise; }; const pizza = makePizza(); console.log(pizza);
In the example above, do you think you will get the pizza ?, or do you think we are going to get the pizzaPromise
?
We get a Promise
, not the ?! Let's add some functionality to our example and see what happens.
function makePizza(toppings) { const pizzaPromise = new Promise(function (resolve, reject) { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`Here is your ? with the toppings ${toppings.join(' ')}`); //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); return pizzaPromise; }; const pepperoniPromise = makePizza(['pepperoni', 'cheese', 'sauce']); const cheesePromise = makePizza(['cheese', 'sauce']); console.log(pepperoniPromise, cheesePromise);
We will get two Promise
s! What do we do if we want the value of the promise though?
function makePizza(toppings) { const pizzaPromise = new Promise(function (resolve, reject) { // Wait 5 seconds for pizza to cook setTimeout(function () { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`Here is your ? with the toppings ${toppings.join(' ')}`)}, 5000); //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); return pizzaPromise; }; const pepperoniPromise = makePizza(['pepperoni', 'cheese', 'sauce']); const cheesePromise = makePizza(['cheese', 'sauce']); pepperoniPromise.then(function (pizza) { console.log('Ahh got it!'); console.log(pizza); });
We nee to use the .then
method to await the promise. The result will be a 5 second wait then: "Ahh got it!", "Here is your ? with the toppings pepperoni cheese sauce".
// Chaining Promises
function makePizza(toppings) { const pizzaPromise = new Promise(function (resolve, reject) { // Wait 5 seconds for pizza to cook setTimeout(function () { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`Here is your ? with the toppings ${toppings.join(' ')}`)}, 5000); //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); return pizzaPromise; }; const pepperoniPromise = makePizza(['pepperoni', 'cheese', 'sauce']); const cheesePromise = makePizza(['cheese', 'sauce']); console.log('Starting'); pepperoniPromise.then(function (pizza) { console.log('Ahh got it!'); console.log(pizza); }); console.log('Finished');
The result will look like this:
This order isn't what we intended and we can fix that with .await
- but we aren't there yet. Right now we know we can chain a .then
to chain a callback. Let's look at more chaining:
function makePizza(toppings) { const pizzaPromise = new Promise(function (resolve, reject) { // Wait 5 seconds for pizza to cook setTimeout(function () { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`Here is your ? with the toppings ${toppings.join(' ')}`)}, 5000); //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); return pizzaPromise; }; makePizza(['pepperoni', 'cheese', 'sauce']) .then(function(pizza){ console.log(pizza); return makePizza(['cheese', 'sauce']); }).then(function (pizza) { console.log(pizza); });
The result of this .then
chain is exactly the same as creating the pepperoniPromise variables. The downside to this is the same issue pointed out before with the console.log('Starting'); and 'Finishing' being out of order.
Note: To chain these
.then
s you have to return what you intend to pass into the next occurrence.
// Async Await
Let's refactor a little and create an empty toppings array into the pizza and then for every topping that is added increase the timeout 200ms.
function makePizza(toppings = []) { return new Promise(function (resolve, reject) { const ammountOfTimeToBake = 500 + (toppings.length *200) // Wait 1 seconds for pizza to cook setTimeout(function () { //WHEN YOU ARE READY, YOU CAN RESOLVE THIS PROMISE resolve(`Here is your ? with the toppings ${toppings.join(' ')}`)}, ammountOfTimeToBake) //IF SOMETHING WENT WRONG, WE CAN REJECT THIS PROMISE }); //REMOVED THE RETURN HERE!!!!!!!!!!!!!!!!!!! }; makePizza(['pepperoni', 'cheese', 'sauce']) .then(function(pizza){ console.log(pizza); return makePizza(['cheese', 'sauce']); }).then(function (pizza) { console.log(pizza); return makePizza(['sauce']); }).then(function (pizza) { console.log(pizza); return makePizza(['1', '2', '3', '4', '5 toppings']); }).then(function (pizza) { console.log(pizza); return makePizza(['1', '2 toppings']); }).then(function (pizza) { console.log('All done! Here is the last pizza: ', pizza); });
Each of these output at their respective completion times for the setTimeout but the order they are written, doesn't matter.
You could make a big promise that shows you all the promises and awaits for them to all finish before outputting using Promise.all()
. Similarly, there is Promise.race()
that will allow you to output the 1st promise that finished then will show you all the rest when they finish like the Promise.all()
.
// Final Thoughts & My Ask
Promises are how we set a chunk of Javascript to run when we want to asynchronously using callbacks in your event loop. The benefits here is the ability to load fast pages that aren't awaiting for a large dataset to load, the ability to continue page loading while the user accepts warnings about turning on their camera, etc. The next article in this series will cover Promises, and Async Await.
I hope you enjoyed reading this as much as I did writing it, if you found this helpful - give this article a retweet and follow @codingwithdrewk on twitter!
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.