0.3 - Callbacks & Promises
šÆ Objectives
- Differentiate between callbacks, promises, and async/await.
- Write an asynchronous program using callbacks functions.
- Write an asynchronous program using promises.
- Substitute promising chaining with the
async
andawait
keywords.
šØ Setup
-
Open Visual Studio Code and open a terminal (
CTRL + `
). -
Create a new folder
mkdir ~/web-ii/exercises/0.3-callbacks-promises
. -
Navigate inside that folder, run
npm init -y
, and add"type": "module"
like previous times. -
Create
pokemonDatabase.js
with the following contents:pokemonDatabase.js export const pokemonDatabase = [{ name: "Bulbasaur", type: "Grass" },{ name: "Squirtle", type: "Water" },{ name: "Charmander", type: "Fire" },];It doesnāt have to be Pokemon name/type! Thatās just what I like. Feel free to use actor/movie, artist/album, author/book, or anything else that you find interesting.
š Context
In E0.1 we saw how we can write non-blocking/asynchronous JavaScript. This has traditionally been done through the use of callback functions. As youāll experience through this exercise, callback functions are prone to a problem known as callback hell where code becomes unreadable and unmaintainable very quickly. Promises were implemented to try and mitigate this problem.
What exactly is a Promise
in JavaScript?
Courtesy of Lydia Hallie. Her article on this topic goes far more in depth than this exercise does. The article is eloquently written and I aspire to reach her level of diagrams. I highly recommend reading it (after finishing this exercise) if youāre curious about the nitty gritty of async programming in JS!
Weāll start by coding our own callback system to asynchronously create new Pokemon in a ādatabaseā (which will just be an array of objects) . From there, weāll turn that into the equivalent structure using promises and explore how we can use promises for both sequential and simultaneous operations. Lastly, weāll simplify the promise by using the async
and await
keywords.
š¦ Letās Go
Callbacks
-
Create a file called
callbacks.js
in the folder for this exercise. -
Import the
pokemonDatabase
from the file created in the setup section above. -
Declare a function named
fetchPokemon()
:- This function will take no parameters.
- Declare a variable called
fetchTime
and initialize it to a random number between 1 and 500.- This number will control how long the
setTimeout
, outlined below, will take to execute. - The randomness is meant to mimic the fluctuation of network speeds.
- This number will control how long the
- This function will print the contents of the
pokemonDatabase
to the terminal afterfetchTime
milliseconds go by. You should use the setTimeout function to achieve this.
-
Declare a function named
createPokemon(pokemon)
:- This function will take one parameter:
pokemon
as an object. - Declare a variable called
createTime
and initialize it to a random number between 1 and 500. This number will control how long thesetTimeout
, outlined below, will take to execute. The randomness is meant to mimic the fluctuation of network speeds. - This function will push the
pokemon
object into thepokemonDatabase
array aftercreateTime
milliseconds go by. You should use the setTimeout function to achieve this.
- This function will take one parameter:
-
Call the
createPokemon
function and pass in an object that contains aname
property and atype
property. The values of these properties are up to you. -
Call
createPokemon
again with a different object. -
Call the
fetchPokemon
function. -
Run the program in the terminal (
node callbacks.js
) several times. If you coded everything correctly until this point, you should see the new Pokemon being printed randomly upon each program execution:This is because weāre using random values for
fetchTime
andcreateTime
. IfcreateTime
is greater thanfetchTime
, it means that thecreatePokemon
function will take longer to run. If it takes longer to run thanfetchPokemon
, then it should make sense that whenfetchPokemon
prints, it does not have the new Pokemon inside ofpokemonDatabase
to print, so it only prints the original three.Ideally, we would like to have the new Pokemon displayed 100% of the time, and in the right order. How can we fix this problem? You guessed it, callbacks! š¤©
-
Modify the
createPokemon
function declaration to take a second parameter calledcallback
. -
Call the
callback()
after the new Pokemon has been inserted into thepokemonDatabase
array. -
Modify the first call to
createPokemon
such that you pass in a callback as the second parameter. That callback should invoke the secondcreatePokemon
where you pass in a new Pokemon object as the first parameter and thefetchPokemon
function as the second parameter. -
Run the program (
node callbacks.js
) several times now and confirm that the new Pokemon are being printed 100% of the time, and in the same order every time.
Callback Hell
The trouble with callback functions is that they can be nested quite easily. When your callback has its own callback that has its own callback, ad infinitum, you can imagine how deep the rabbit hole can go!
This phenomenon has been affectionately dubbed as ācallback hellā and itās one of the main reasons why developers came up with promises! š®
Promises
Letās see how we can get rid of callback hell by using promises.
-
Make a duplicate of
callbacks.js
and call itpromises.js
. -
At the end of the
createPokemon
function, return anew Promise()
:- The promise will take one parameter as input: a callback function.
- The callback will take 2 parameters as input called
resolve
andreject
. - Move the
setTimeout
function entirely into the body of the promiseās callback. - Change
callback()
toresolve()
inside ofsetTimeout
. - Remove the
callback
parameter from the parameter list ofcreatePokemon
.
-
Remove the calls to
createPokemon
andfetchPokemon
from the bottom of this file and replace them with one call tocreatePokemon
, passing in the one Pokemon object it normally takes. -
Now, because
createPokemon
returns aPromise
object, we can call thethen()
method on it.- We can pass in a callback to
then()
and that callback will be invoked if the previousPromise
was successfully fulfilled. - If the return value of the callback passed to
then()
returns a promise, we can chain anotherthen()
onto it. This secondthen()
can take a new callback, which can return a promise, so we can chain anotherthen()
, ad infinitum.
- We can pass in a callback to
-
Run this program (
node promises.js
) several times now and confirm that the new Pokemon are being printed 100% of the time, and in the same order every time.
Promise.all
Chaining promises using then
is great if we want multiple operations to run sequentially, one after the next. However, by doing this, we lose the ability to have multiple operations run simultaneously, at the same time. Letās look at how we can perform operations simultaneously using Promise.all()
!
-
Make a duplicate of
promises.js
and call itpromise-all.js
. -
Inside of
promise-all.js
, remove the block of chained promises from the end of the file. -
Declare a new array called
pokemonPromises
. Each element of this array will be a call tocreatePokemon
. -
After the array, call
Promise.all(pokemonPromises)
. This function takes an array of promises, and waits for all promises in the array to be fulfilled. Once all promises in the array are fulfilled,Promise.all
returns a promise of its own. We can callthen
on this returned promise to execute something after all the simultaneous operations have finished. In this case, we want to executefetchPokemon
after all the Pokemon have been created. -
Run this program (
node promise-all.js
) several times and confirm that the new Pokemon are being printed 100% of the time, but not in the same order every time. It should make sense why the output is different each time you run the program. Since we are running allcreatePokemon
operations at the same time, and each one of them takes a different time to run, we will get a different output each time.
We can compare the time difference between sequential operations and simultaneous operations by using console.time()
:
The execution time was cut in half! š
Async/Await
After a couple of years of using promises, JS developers found it cumbersome to have to chain the .then()
calls. Theyāre definitely better than using callbacks, but it would be nice if we could write asynchronous code in the same way we write synchronous code. In an attempt to do this, the keywords async
and await
were born. These keywords are syntactic sugar for using promises.
-
Make a duplicate of
promises.js
and call itasync-await.js
. -
Inside of
async-await.js
, remove the block of chained promises from the end of the file. -
Declare a function called
createAllPokemon()
:- In the body of the function, call
createPokemon
, passcreatePokemon
a new object, but donāt callthen()
oncreatePokemon
. Instead, add the keywordawait
in front of the call.- The
await
keyword is essentially a (nicer looking) replacement for usingthen()
. The program will wait for this promise to be fulfilled before moving on with execution.
- The
- Repeat this for several more calls to
createPokemon
. - Finally, call
fetchPokemon
. SincefetchPokemon
does not return a promise, we donāt have toawait
it.
- In the body of the function, call
-
We can only use
await
inside of a function that has been declared asasync
. To do this, write the keywordasync
before the function declaration. -
Call
createAllPokemon
at the end of the file. -
Run this program (
node async-await.js
) several times and confirm that the new Pokemon are being printed 100% of the time, and in the same order every time.
š„ Submission
Take a screenshot of all four programs being run with the time that each program took to execute and submit it on Moodle. Again, you can use console.time()
to accomplish this. Put console.time('timer label')
before calling any function, and put console.timeEnd('timer label')
inside of fetchPokemon
just after it finishes printing all the Pokemon to the terminal.
Submit the screenshot in the Moodle drop box called Exercise 0.3 - Callbacks & Promises.
Asynchronous programming can be tough to wrap your head around, thatās for sure. Hopefully you can appreciate the advantages it provides and how it unlocks a whole other world of possibilities!