Marcy Lab School Docs
  • Welcome
  • Student Guidelines & Policies
    • Student Handbook
    • AI Policy
    • Academic Calendar
  • Environment Setup
    • Local Environment Setup - Mac
    • Local Environment Setup - Windows
    • GitHub Setup
    • Postgres Setup
  • How-Tos
    • How To Code at Marcy: Code Style Guide
    • How to Do Short Response and Coding Assignments
    • How to Debug
    • How to PEDAC
    • How to Create A GitHub Organization and Scrumboard
    • How to Create Projects with Vite
    • How to Deploy on GitHub Pages
    • How to Deploy on Render
    • How to Test your API with Postman
  • Mod 0 - Command Line Interfaces, Git, and GitHub
    • Overview
    • 1. Command Line Interfaces
    • 2. Git & GitHub
    • 3. Git Pulling & Merging
    • 4. Git Branching & PRs
  • Mod 1 - JavaScript Fundamentals
    • Overview
    • 1. Intro to Programming
    • 2. Errors
    • 3. Node & Node Modules
    • 4. Variables, Functions & String Methods
    • 5. Control Flow, typeof, and Math
    • 6. Loops
    • 7. Arrays
    • 8. Objects
    • 9. Higher Order Functions: Callbacks
    • 10. Higher Order Functions: Array Methods
    • 11. Regex
  • Mod 2 - HTML, CSS & the DOM
    • Overview
    • 1. HTML
    • 2. CSS
    • 3. Accessibility (a11y)
    • 4. The Document Object Model (DOM) API
    • 5. Events
    • 6. Forms
    • 7. The Box Model and Positioning
    • 8. Flexbox
    • 9. Grid & Media Queries
    • 10. ESModules
    • 11. Vite
    • 12. LocalStorage
  • Mod 3 - Async & APIs
    • Overview
    • 1. Promises
    • 2. Fetch
    • 3. Building a Fetching App
    • 4. Async & Await
    • 5. A Generic Fetch Handler
  • Mod 4 - Project Week!
    • Important How Tos and Guides
      • How to Create a GitHub Organization and Scrum Board
      • How To Start a Project with Vite
      • How To Deploy a Project with GitHub Pages
    • Project Week Overview
    • Agile Methodologies
    • Deliverables & Milestones
    • Technical Requirements Checklist
    • Free API List
    • Collaborative GitHub
  • Mod 5 - Object-Oriented Programming
    • Overview
    • 1. Intro to OOP, Encapsulation, Factory Functions, and Closure
    • 2. Classes
    • 3. Private & Static
    • 4. UML Diagrams & Has Many/Belongs To Relationships
    • 5. Challenge: Implementing Has Many/Belongs To
    • 6. Inheritance
    • 7. Polymorphism
    • 8. Review and Practice
    • MDN: Object Prototypes
  • Mod 6 - Data Structures & Algorithms
    • Overview
    • Important How Tos and Guides
      • How to Debug
      • How to PEDAC
    • 1. Nodes & Linked Lists
    • 2. Singly & Doubly Linked Lists
    • 3. Stacks & Queues
    • 4. Recursion
    • 5. Trees
  • Mod 7 - React
    • Overview
    • Important How Tos and Guides
      • How to Create Projects with Vite
      • How to Deploy on GitHub Pages
    • 1. Intro to React
    • 2. Events, State, and Forms
    • 3. Fetching with useEffect
    • 4. React Router
    • 5. Building a Flashcards App
    • 6. React Context
    • 7. Global Context Pattern
  • Mod 8 - Backend
    • Overview
    • Important How Tos and Guides
      • How to Deploy on Render
      • How to Test your API with Postman
      • Postgres Setup
    • 1. Intro to Express
    • 2. Building a Static Web Server with Middleware
    • 3. Securing API Keys and Environment Variables
    • 4. RESTful CRUD API
    • 5. Model-View-Controller Architecture
    • 6. SQL and Databases
    • 7. JOIN (Association) SQL Queries
    • 8. Knex
    • 9. Your First Fullstack App!
    • 10. Migrations & Seeds
    • 11. Schema Design & Normalization
    • 12. Hashing Passwords with Bcrypt
    • 13. React Express Auth Template Overview
  • Mod 9 - Civic Tech Hackathon
    • Overview
    • Rubric
  • Mod 10 - Capstone
    • Overview
Powered by GitBook
On this page
  • Synchronous vs. Asynchronous Functions and setTimeout
  • Executing Asynchronous Code With Callbacks
  • Sequential Asynchronous Callbacks Leads to Callback Hell
  • Promises!
  • Sequential Asynchronous Functions With Promises
  • Promise.all()
  • Making Promises
  • A Function that Makes and Returns a Promise
  • Summary
  • Coming up...
  1. Mod 3 - Async & APIs

1. Promises

PreviousOverviewNext2. Fetch

Last updated 5 months ago

Follow along with code examples !

Table of Contents

Synchronous vs. Asynchronous Functions and setTimeout

Most 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:

console.log("starting the first task");
setTimeout(() => { // this task takes 3 seconds
  console.log("first task completed")
}, 3000)

console.log("starting the second task");
setTimeout(() => { // this task takes 1 seconds
  console.log("second task completed")
}, 1000)

console.log("starting the third task");
setTimeout(() => { // this task takes 2 seconds
  console.log("third task completed")
}, 2000)
Q: In what order will the console log statements be executed? What happens if we set the timeouts to 0 milliseconds?
starting the first task
starting the second task
starting the third task
second task completed
third task completed
first task completed

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.

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!

console.log('start');

document
  .querySelector('#clickButton')
  .addEventListener('click', () => {
    console.log('the button has been clicked!');
  });

console.log('end');

No matter how quickly the user clicks the button, the order will always be:

start
end
the button has been clicked

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.

"name","age","email"
"kobe","25","kobe@mail.com"
"lebron","39","lebron@mail.com"
"sue","42","sue@mail.com"

The fs.readFile function lets us import the contents of a file by providing:

  1. A filepath

  2. A callback to execute when fs.readFile is 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:

// the fs module has functions for interacting with the file system
const fs = require('node:fs');

console.log('Reading the booksHuge.csv file');

fs.readFile('../data/booksHuge.csv', 'utf-8', (err, data) => {
  // this code is scheduled to run when the file is done being processed
  if (err) {
    console.error(err);
  } else {
    const lines = data.split('\n').length;
    console.log(`Done reading the booksHuge.csv file. There were ${lines - 1} books.`);
  }
});

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 data contains 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:

console.log("starting the first task!");
setTimeout(() => {
  console.log("first task completed");
  // now start the next task
  console.log("starting the second task!");
  setTimeout(() => {
    console.log("second task completed");
    // now start the final task
    console.log("starting the third task!");
    setTimeout(() => {
      console.log("third task completed");
    }, 2000);
  }, 1000);
}, 3000);

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 .then to be invoked when the promise "resolves" (suceeds and the data is provided).

  • We provide a callback to .catch to be invoked if the promise "rejects" (fails and the err is provided).

  • In both cases, the callbacks are executed asynchronously.

// Now, we'll use the promises version of fs
const fs = require('node:fs/promises');

console.log("Reading the-raven.txt");

// A Promise object is returned
const promise = fs.readFile('../data/the-raven.txt', 'utf8');

// The .then method takes a callback to execute when the promise resolves (succeeds)
// The .catch method takes a callback to execute when the promise rejects (fails)
promise
  .then((data) => {
    const ravenCount = data.match(/raven/gi).length;
    console.log(`Done reading the-raven.txt.`)
    console.log(`There were ${ravenCount} mentions of "Raven".`);
  })
  .catch((err) => {
    console.log('Something went wrong!');
    console.error(err);
  });

// The object on its own looks like this: Promise { <pending> }
console.log(promise);
// Here, we use the default callback version of fs
const fs = require('node:fs');

console.log("Reading the-raven.txt");

// We have to provide a callback as the third argument
fs.readFile('../data/the-raven.txt', 'utf-8', (err, data) => {
  if (err) {
    console.log('Something went wrong!');
    console.error(err);
  } else {
    const ravenCount = data.match(/raven/gi).length;
    console.log(`Done reading the-raven.txt.`)
    console.log(`There were ${ravenCount} mentions of "Raven".`);
  }
});

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.

const fs = require('node:fs');

console.log("Reading the booksHuge.csv file");

fs.readFile('../data/booksHuge.csv', 'utf-8', (err, data) => {
  if (err) {
    console.error(err);
  } else {
    const lines = data.split('\n').length;
    console.log(`Done reading the booksHuge.csv file. There were ${lines - 1} books.`);

    console.log("Reading the books.csv file");
    fs.readFile('../data/books.csv', 'utf-8', (err, data) => {
      if (err) {
        console.error(err);
      } else {
        const lines = data.split('\n').length;
        console.log(`Done reading the books.csv file. There were ${lines - 1} books.`);
      }
    });
  }
});

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.

const fs = require('node:fs/promises');

console.log("Reading the booksHuge.csv file");

// Often, we will just call .then directly on the function call
fs.readFile('../data/booksHuge.csv', 'utf-8')
  .then((data) => {
    const lines = data.split('\n').length;
    console.log(`Done reading the booksHuge.csv file. There were ${lines} lines.`);
    
    // start the next async process
    console.log("Reading the books.csv file next");

    // the promise returned here is handled by the next .then()
    return fs.readFile('../data/books.csv', 'utf-8');
  })
  .then((data) => {
    // handle the data from books.csv
    const lines = data.split('\n').length;
    console.log(`Done reading the books.csv file. There were ${lines} lines.`);
  })
  .catch((err) => {
    // We only need one error handler for both asynchronous function calls
    console.error(err);
  });

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:

const fs = require('node:fs/promises');

// First, start both processes and save the promises
console.log("Reading both files!");
const booksHugePromise = fs.readFile('../data/booksHuge.csv', 'utf-8')
const booksPromise = fs.readFile('../data/books.csv', 'utf-8')

// Then, put the promises into an array
const promises = [booksHugePromise, booksPromise]

// Promise.all returns a promise that settles once all of the provided promises have settled
Promise.all(promises)
  .then((values) => {
    // values will contain the resolved values of the provided promises
    const booksHugeLines = values[0].split('\n').length;
    const booksLines = values[1].split('\n').length;
    console.log(`There were ${booksHugeLines} lines in booksHuge.csv!`);
    console.log(`There were ${booksLines} lines in books.csv!`);
  })
  .catch((err) => {
    // the err value will be from the first promise that fails
    console.error(err);
  });

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.all resolve, the callback provided to .then will 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:

const myPromise = new Promise((resolve, reject) => {
  // asynchronous functions here
});
  • 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: resolve and reject.

    • 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:

// The callback determines when and how the promise resolves (or rejects)
const rollPromise = new Promise((resolve, reject) => {
  // wait 5 seconds before settling the promise
  setTimeout(() => {
    const roll = Math.ceil(Math.random() * 6);

    // In this case, rolls 3, 4, 5, or 6 will resolve the promise
    // Rolls 1 or 2 will reject the promise
    if (roll > 2) {
      resolve(`Success! ${roll} was rolled`);
    } else {
      reject(`Failure. ${roll} was rolled`)
    }
  }, 5000);
});

// schedule callbacks to execute when the promise resolves or rejects
rollPromise
  .then((successMessage) => {
    console.log(successMessage);
    // Prints "Success! X was rolled" (where X is a number 3-6)
  })
  .catch((failureMessage) => {
    console.error(failureMessage);
    // Prints "Failure. X was rolled" (where X is 1 or 2)
  })

Let's break down this example:

  • We declare rollPromise to hold the new Promise object being created. We use this variable to define resolve/reject handlers with .then and .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 resolve with a success message.

    • Otherwise, we invoke reject with a failure message.

  • The value we invoke resolve with is passed to the .then handler which we've decided to print with console.log.

  • The value we invoke reject with is passed to the .then handler which we've decided to print with console.error.

A Function that Makes and Returns a Promise

Here is an asynchronous function that returns a promise that ALWAYS resolves

const makeGuarantee = () => {
  return new Promise((resolve, reject) => {
    resolve('100% of the time, it works every time');
  });
}

makeGuarantee().then(data => {
  console.log("I dont even need a catch since it will always resolve");
  console.log(data);
});

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:

  1. Start the asynchronous function and get a Promise back (order your pizza, get your ticket)

  2. 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:

// Execute async code that returns a Promise
const fetchPromise = fetch('https://pokeapi.co/api/v2/pokemon/pikachu');

// Define what to do with the resolved Promise
fetchPromise
  .then((response) => response.json())
  .then((jsonData) => console.log(jsonData))
  .catch((error) => console.error(error.message))

Even if we set all of the timers to 0, synchronous functions will always complete first, then asynchronous functions. This is due to .

A (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).

how the event loop works
character encoding
here
Synchronous vs. Asynchronous Functions and setTimeout
Executing Asynchronous Code With Callbacks
Sequential Asynchronous Callbacks Leads to Callback Hell
Promises!
Sequential Asynchronous Functions With Promises
Promise.all()
Making Promises
A Function that Makes and Returns a Promise
Summary
Coming up...
Diagram showing the flow of Promises