click to view as a presentation
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
- Setup
- The Use Case of Promises
- What's a Promise?
- Making Promises
- Resolving Promises
- Rejecting Promises
- Chaining Promises
- Example - Seeding a Database
-
In Terminal,
cd
to the class repo. -
Get the latest updates from the remote class repo:
$ git pull upstream master
-
cd
into the.../work/w05/d2/01-js-promises/starter-code/mongoose-movies
folder. -
Open the
mongoose-movies
folder in your code editor. -
Install the Node modules:
$ npm install
-
Promises provide another way to deal with asynchronous code execution.
-
💪 Pair up and take two minutes to answer the following:
- What functions/methods have we used that execute asynchronously?
- What "mechanism" have we used that enables code to be run after an asynchronous operation is complete?
-
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)
-
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...
-
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...
-
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
andreject
parameters. - The promise created is an object with a
<pending>
state.
- The executor is immediately called by the Promise constructor passing functions as args for the
-
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.
-
So, how does a promise become
fulfilled
?
By calling theresolve
function:const p = new Promise(function(resolve, reject) { let value = 42; resolve(value); });
-
The promise,
p
, has been resolved with the value42
. -
Note that promises can only be resolved with a single value, however that value can be anything such as an object, etc.
-
How do we get the value of a resolved promise?
By calling the promise'sthen
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 callthen
multiple times to access the value of a resolved promise.
-
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 thereject
function does...
-
Now let's call the
reject
function instead ofresolve
: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...
-
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...
- We've covered the fundamentals of promises. Next we'll see how we can chain multiple promises. But first...
-
As a way of working with asynchronous operations, promises provide an alternative to _________ functions.
-
What three states can a promise be in?
-
What method do we call on a promise to obtain its resolved value?
-
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.
-
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...
-
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).
-
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 nextthen
is called when that promise resolves.
-
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...
-
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.
-
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...
-
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.
-
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...
-
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...
-
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!
-
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...
-
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.
-
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.
-
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?
-
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); });
-
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...
-
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...