4. Promises
Follow along with code examples here!
Table of Contents
Synchronous vs. Asynchronous Functions and setTimeout
setTimeoutMost of the functions we have used are synchronous functions.
The order in which synchronous functions execute is sequential, meaning we must complete the execution of one function before starting to execute the next.
const start = () => console.log('start');
const longTask = () => {
// runs 10 billion times!
for (let i = 0; i < 10000000000; i++) { }
console.log(`done looping 10 billion times!`);
}
const end = () => console.log('end');
start();
longTask();
end();Depending on how fast your computer is, this program could take a while to print out 'end' for the very reason that the end() function can't start until longTask is complete.
Asynchronous functions do not execute sequentially. Instead, they start a process (typically one that takes time) but do NOT prevent subsequent code from running. This is why it is referred to as "non-blocking".
Often, asynchronous functions accept a callback to execute when the long process is completed. We can see this clearly with setTimeout which starts a timer and executes a callback when the timer is up:
Q: In what order will the console log statements be executed? What happens if we set the timeouts to 0 milliseconds?
As a result, the "starting the X task" statements are printed first and in the order in which they were written. Then, as each timer finishes, the console.log statements are executed asynchronously.
The scheduling of each callback happens sequentially but the actual invocation of the callbacks happens asynchronously, that is, when each timer reaches 0.
Even if we set all of the timers to 0, synchronous functions will always complete first, then asynchronous functions. This is due to how the event loop works.
The main benefit of asynchronous functions is that they can be started and then completed at a later time without causing the rest of the program to wait.
Other examples of asynchronous operations include:
processing an image
fetching data over the internet
counting down a timer
waiting for a user to click on a button.
Q: setTimeout uses a callback to handle the completion of the timer. Which example from that list have we already used that also uses callbacks?
Event handlers!
No matter how quickly the user clicks the button, the order will always be:
In the code snippet above, the callback provided to addEventListener is an example of an asynchronous function.
Executing Asynchronous Code With Callbacks
Another great example of an asynchronous function using callbacks is the asynchronous fs.readFile function. The fs module is only available in Node (not in the browser) and it has functions that allow the programmer to access files in their filesystem.
Take a look at the data/booksHuge.csv file, it contains data about books. Each line in that file represents a different book and we will try write a program to count how many lines (books) there are.
A comma-separated value (CSV) file is a simple way to represent large datasets in rows and columns. Each row in a CSV file represents a single entry in the table (except the first line which defines the names of each column). Each entry contains data separated by some uniform character like a comma , or semicolon ;.
For example, this CSV file has three columns and 3 entries.
The fs.readFile function lets us import the contents of a file by providing:
A filepath
A character encoding (in the example we use
'utf-8'which basically means "read the file as normal text" and is the encoding used 99% of the time).A callback to execute when
fs.readFileis done reading the file.
Since reading a file could potentially take a long time, we use the callback to "schedule" a handler function to run when the file has been completely read:
In this example, we are reading a file called booksHuge.csv.
When the file has been read completely, the callback is executed.
We first check to see if an error occurred, printing it out if it has.
If no error exists, the
datacontains the contents of the file as a single string. We check to see how many lines are in the file and print it out!
Sequential Asynchronous Callbacks Leads to Callback Hell
Okay, so asynchronous callbacks are great for executing time-consuming tasks without blocking our synchronous code.
But what if we DO want our asynchronous code to be blocking. That is, to wait for one function to finish before starting the next, just like synchronous code?
With callbacks, we are forced to do something like this, appropriately called callback hell:
Yikes, that is hard to read! Here is what is happening:
Start the first task
After 3 seconds, the first task will complete and we start the second task
After 1 seconds, the second task will complete and we start the third task
After 2 seconds, the third task will complete
Promises!
Promises provide an alternate approach to callbacks, helping us avoid callback hell.
Consider the example below which uses the Promises version of the fs.readFile function.
The Promise version of readFile returns a Promise object instead of taking in a callback (toggle between the two versions to compare them)
We provide a callback to
.thento be invoked when the promise "resolves" (suceeds and thedatais provided).We provide a callback to
.catchto be invoked if the promise "rejects" (fails and theerris provided).In both cases, the callbacks are executed asynchronously.
A Promise is a type of object (a "class" of object) that represents a pending asynchronous operation.
A Promise object can exist in one of three states:
pending - the function is still in process.
resolved - the function was a success! We got the value we wanted. Also referred to as "fulfilled”
rejected - the function failed. We got an error back.
The
Promise.then()method schedules a callback to be executed when the promise resolves.The
Promise.catch()method schedules a callback to be executed when the promise rejects.Promise.then()returns a Promise allowing it to be chained.
Sequential Asynchronous Functions With Promises
At this point, the code isn't dramatically different. However, Promises start to shine when we want to perform multiple asynchronous functions sequentially.
Compare and contrast these two approaches for reading the booksHuge.csv file first followed by the books.csv file second:
As you can see in the callbacks version of the code, we very quickly get to four levels of indentation, causing the readability of our code suffer.
With promises, since .then calls can be chained together, we limit the indentation to two levels. This is only possible because the first .then returns a promise that can be handled by the second .then. Additionally, we only need to write one error handler with .catch which can handle errors that occur at any point in the chain of promises.
Promise.all()
In the previous example, we wait for the first fs.readFile() to finish before starting the second fs.readFile() call. In between, we were able to run some console.log statements.
However, if we want to wait until BOTH operations are completed before handling them, we can use the Promise.all function:
Promise.all takes in an array of promise objects and returns a promise that settles once all of the provided promises have settled.
The resulting values of each promise are added to an array.
When all promises provided to
Promise.allresolve, the callback provided to.thenwill be invoked with the array of resolved values.If any of the promises reject (fail), the
.catch()callback will be invoked with the error of the first failing promise.
Making Promises
To better understand functions that return promises, we can make a Promise ourselves!
Remember, a Promise represents an asynchronous operation that will take some time to complete. When it completes (when it "resolves") or when it fails (when it "rejects"), we can decide what to do next using callbacks provided to .then and .catch
To create a Promise, use the new Promise() constructor function:
The
new Promise()constructor function takes in a callback that contains the asynchronous functions to be performed.The provided callback will be given two functions as parameters:
resolveandreject.Invoke
resolve(successValue)to indicate that the asynchronous function succeeds.Invoke
reject(failureValue)to indicate failure.
In the example below, we create a new Promise that starts a 5 second timer (that's our asynchronous operation). When the timer is done, a random number from 1-6 is generated and the Promise either resolves or rejects depending on the result. Below the creation of the Promise, we schedule callbacks to handle the value produced in each scenario:
Let's break down this example:
We declare
rollPromiseto hold the newPromiseobject being created. We use this variable to define resolve/reject handlers with.thenand.catch.Within the callback provided to
new Promise(), the asynchronous function is a timer counting down from 5.When the timer is done, we will "roll a die".
If the die roll is greater than 2, we invoke
resolvewith a success message.Otherwise, we invoke
rejectwith a failure message.
The value we invoke
resolvewith is passed to the.thenhandler which we've decided to print withconsole.log.The value we invoke
rejectwith is passed to the.thenhandler which we've decided to print withconsole.error.
A Function that Makes and Returns a Promise
Here is an asynchronous function that returns a promise that ALWAYS resolves
Most often, you don’t create Promises yourself. You’ll just "consume" them from functions like fs.readFile or fetch.
Q: Is it possible for an asynchronous function to complete before a synchronous function?
Synchronous code will always be executed before asynchronous code.
Summary
Using a Promise involves two steps:
Start the asynchronous function and get a Promise back (order your pizza, get your ticket)
Define how to handle the resolved/rejected Promise using
.then()and.catch()(when ready, I will hand in my ticket and get my pizza)
Imagine walking into a Pizza shop and you ask for a slice of cheese pizza. The pizza isn’t ready yet so you have to wait. The person at the register gives you a ticket to claim your slice when it is ready. Meanwhile, you are free to run other errands and can return later to pick up your pizza. You get notified that the pizza is done so you return to the shop, hand in your ticket, and take your pizza home.
Q: In the pizza shop example, what is the promise object we are given? what is the resulting value?
The ticket is the promise. The resulting value is the pizza (or them telling us that they ran out of pizza).
Coming up...
Next time, we’ll write code like this:
Last updated