Skip to content

Instantly share code, notes, and snippets.

@jim-clark
Last active October 17, 2020 06:05
Show Gist options
  • Save jim-clark/c79f0ee9dd1f4b62f81fea41c15add24 to your computer and use it in GitHub Desktop.
Save jim-clark/c79f0ee9dd1f4b62f81fea41c15add24 to your computer and use it in GitHub Desktop.

click to view as a presentation


JavaScript Promises


Learning Objectives


Students Will Be Able To:

  • Describe the Use Case for Promises
  • Create a Promise
  • Run code when a Promise resolves
  • Run code when a Promise is rejected
  • Chain Promises
  • Seed a Database

Roadmap


  1. Setup
  2. The Use Case of Promises
  3. What's a Promise?
  4. Making Promises
  5. Resolving Promises
  6. Rejecting Promises
  7. Chaining Promises
  8. Example - Seeding a Database

Setup

  1. In Terminal, cd to the class repo.

  2. Get the latest updates from the remote class repo:

    $ git pull upstream master
  3. cd into the
    .../work/w05/d2/01-js-promises/starter-code/mongoose-movies folder.

  4. Open the mongoose-movies folder in your code editor.

  5. Install the Node modules:

    $ npm install

The Use Case of Promises


  • Promises provide another way to deal with asynchronous code execution.

  • 💪 Pair up and take two minutes to answer the following:

    1. What functions/methods have we used that execute asynchronously?
    2. What "mechanism" have we used that enables code to be run after an asynchronous operation is complete?

The Use Case of Promises


  • Promises provide an alternative to callbacks as a way to work with asynchronous code execution.

  • Functions/methods that implement asynchronous operations must be written to either:

    • Accept a callback
    • Return a promise
    • Or do both (Mongoose queries are an example of this)

What's a Promise?


  • A promise is a special JavaScript object.

  • A promise represents the eventual completion, or failure, of an asynchronous operation.

  • Although we usually consume promises returned by functions, we'll start by creating a promise so that we can better see how they work...


Making Promises


  • In the seeds.js file, let's make a promise using the Promise class/constructor:

     const p = new Promise();
  • Saving and running in terminal:

     $ node seeds

    Will generate an error because a function argument must be passed in...


Making Promises


  • Let's give new Promise() an executor function as an argument that has two parameters:

     const p = new Promise(function(resolve, reject) {
       console.log(resolve, reject);
     });
     console.log(p);
  • Observations:

    • The executor is immediately called by the Promise constructor passing functions as args for the resolve and reject parameters.
    • The promise created is an object with a <pending> state.

Making Promises


  • A promise is always in one of three states:

    • pending: Initial state, neither fulfilled nor rejected.
    • fulfilled: The async operation completed successfully.
    • rejected: The async operation failed.
  • Once a promise has been settled, i.e., it's no longer pending, its state will not change again.


Resolving Promises


  • So, how does a promise become fulfilled?
    By calling the resolve function:

     const p = new Promise(function(resolve, reject) {
       let value = 42;
       resolve(value);
     });
  • The promise, p, has been resolved with the value 42.

  • Note that promises can only be resolved with a single value, however that value can be anything such as an object, etc.


Resolving Promises


  • How do we get the value of a resolved promise?
    By calling the promise's then method:

     const p = new Promise(function(resolve, reject) {
       let value = 42;
       resolve(value);
     });
     
     p.then(function(result) {
       console.log(result);
     });
  • The then method will execute the callback as soon as the promise is resolved. BTW, you can call then multiple times to access the value of a resolved promise.


Resolving Promises


  • So far our code is synchronous, let's make it asynchronous:

     const p = new Promise(function(resolve, reject) {
       setTimeout(function() {
         resolve('Timed out!');
       }, 2000);
     });
     
     p.then(function(result) {
       console.log(result);
     });

    We're using setTimeout to create an asynchronous operation

  • So, we've seen how the resolve function fulfills a promise,
    I bet you know what the reject function does...


Rejecting Promises


  • Now let's call the reject function instead of resolve:

     const p = new Promise(function(resolve, reject) {
       setTimeout(function() {
         reject('Something went wrong!');
       }, 2000);
     });
  • After 2 seconds, we'll see a UnhandledPromiseRejectionWarning: ... error.

  • Reading the error more closely reveals that we need a .catch() to handle the promise rejection...


Rejecting Promises


  • Let's chain a catch method call:

     p.then(function(result) {
       console.log(result);
     }).catch(function(err) {
       console.log(err);
     });
  • That's better!

  • The next slide shows a graphic summarizing what we've learned so far about promises...


Promises - Review

  • We've covered the fundamentals of promises. Next we'll see how we can chain multiple promises. But first...

❓ Promises - Review Questions


  1. As a way of working with asynchronous operations, promises provide an alternative to _________ functions.

  2. What three states can a promise be in?

  3. What method do we call on a promise to obtain its resolved value?


Chaining Promises


  • Do you remember having to nest callback functions?

  • It can get ugly:

  • The advantage of promises is that they "flatten" the async flow and thus avoid the so-called pyramid of doom.


Chaining Promises


  • We can chain as many .then methods we want:

     p
     .then(function(result) {
       console.log(result);
       return 42;
     })
     .then(function(result) {
       console.log(result);
       return 'Done!'
     })
     .then(function(result) {
       console.log(result);
     });
  • Let's see what happens if we return promises instead of primitives...


Chaining Promises


  • First we need a cool function with an asynchronous operation:

     function asyncAdd(a, b, delay) {
       return new Promise(function(resolve) {
         setTimeout(function() {
           resolve(a + b);
         }, delay);
       });
     }
  • The function returns a promise that resolves to the result of adding two numbers after a delay (ms).


Chaining Promises


  • This code demonstrates promise chaining in action:

     asyncAdd(5, 10, 2000)
     .then(function(sum) {
       console.log(sum);
       return asyncAdd(sum, 100, 1000);
     })
     .then(function(sum) {
       console.log(sum);
       return asyncAdd(sum, 1000, 2000);
     })
     .then(function(sum) {
       console.log(sum);
     });
  • Note how when the then callback returns a promise, the next then is called when that promise resolves.


Chaining Promises


  • Nice!

  • We've made our own promises, resolved them, and chained them!

  • More commonly though, we'll be consuming promises returned by libraries such as Mongoose...


Example - Seeding a Database


  • Seeding a database is the process of populating a database with some initial data.

  • Use cases for seeding a database include:

    • Creating an initial admin user
    • To provide data for lookup tables/collections. For example, in a inventory app for a grocery store, you might seed a departments table/collection with values like Deli, Dairy, Bakery, Meat & Seafood, etc.
  • The code to seed a database is external to the applications that use the database and is executed separately.


Example - Seeding a Database


  • At the top of seeds.js, let's connect to the database, require the Models and load the data module:

     // utility to initialize database
     require('./config/database');
     const Movie = require('./models/movie');
     const Performer = require('./models/performer');
     const data = require('./data');
  • To avoid duplicates when seeding a database, we first need to delete all data from the collections we'll be inserting data into...


Example - Seeding a Database


  • The following code deletes all movie documents and correctly ends the program:

     // clear out all movies and performers to prevent dups
     Movie.deleteMany({})
     .then(function(results) {
       console.log(results);
       process.exit();
     });

    No callback provided to the deleteMany method!

  • Run $ node seeds and you'll see the result object logged out.


Example - Seeding a Database


  • Most Mongoose Model methods return a "thenable" that works like a promise. That means we can chain the code to delete performers:

     Movie.deleteMany({})
     .then(function(results) {
       console.log('Deleted movies: ', results);
       return Performer.deleteMany({});
     })
     .then(function(results) {
       console.log('Deleted performers:', results);
     })
     .then(function() {
       process.exit();
     });
  • The above works, but there's a better way...


Example - Seeding a Database


  • There's nothing wrong with the code as written - it works. However, the code first deletes movies, then afterwards, deletes the performers in series.

  • Because they are not dependent upon each other, it would be more efficient to perform both operations simultaneously - the Promise.all method can make this happen...


Example - Seeding a Database


  • Promise.all accepts an array of promises and returns a single promise in their place:

     // clear out all movies and performers to prevent dups
     const p1 = Movie.deleteMany({});
     const p2 = Performer.deleteMany({});
     Promise.all([p1, p2])
     .then(function(results) {
       console.log(results);
     })
     .then(function() {
       process.exit();
     });
  • The above code now removes documents from the movies & performers collections in parallel!


Example - Seeding a Database


  • Finally, let's create some data, beginning with performers:

     ...
     Promise.all([p1, p2])
     .then(function(results) {
       console.log(results);
       return Performer.create(data.performers);
     })
     .then(function(performers) {
       console.log(performers);
     })
     .then(function() {
       process.exit();
     });
  • Try it out. Now it's your turn...


Example - Seeding a Database


  • data.movies contains an array of movie data.

  • 💪 YOU DO: Add the code that will create the movie documents.

  • Click the chili pepper when you've finished.


Example - Seeding a Database


  • Spinning up the server and browsing to localhost:3000 verifies the data is looking sweet.

  • Although using the app to assign performers to a movie's cast property is fun, let's take a look at how you might do it in seeds.js.


Example - Seeding a Database


  • Important: You should never refer to an actual _id within the code in a seeds file.
    For example, don't write code like:

     Movie.findById('5c609ac7641fdd63f6b8b71d')
     .then(...)
  • Why?


Example - Seeding a Database


  • Instead, we have to query for documents based on properties other than _id.

  • For example:

     // find all PG-13 movies
     Movie.find({mpaaRating: 'PG-13'})
     .then(function(movies) {
       console.log(movies);
     });

Example - Seeding a Database


  • Let's say we want to assign the performer, Mark Hamill, to the movie, Star Wars - A New Hope.

  • The code on the following slide uses another Promise.all because we can't resolve more than one value...


Example - Seeding a Database

  • We'll review as we type this:

     .then(function(movies) {
       return Promise.all([
         Performer.findOne({name: 'Mark Hamill'}),
         Movie.findOne({title: 'Star Wars - A New Hope'})
       ]);
     })
     .then(function(results) {  // one day we'll destructure this!
       const mark = results[0];
       const starWars = results[1];
       starWars.cast.push(mark);
       return starWars.save();
     })
     .then(function() {
       process.exit();
     });
  • Check it out in the app - congrats! On to the lab...


References


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment