Open CheatSheet Back To List Next Tutorial

Promises

Intro

  • This tutorial covers JavaScript Promises.

  • It is recommended that you follow along with each section below. Create a folder called promises. Paste or type the code into your own JavaScript and HTML files. 

  • The example code for the entire tutorial series is at github.com/LearnByCheating/javascript-tutorial. There is a separate folder called promises that holds the files from this tutorial. The Readme file has instructions on how to run the code in the Browser environment.

We will cover the following topics and walk through example source code:
  • Definitions: What are promises? What is the difference between synchronous and asynchronous code?
  • How to use promises that are already built into existing Web APIs and Node npm packages. Including the two formats available for consuming Promises: 
    1. Thenables: using the then, catch, and finally methods.
    2. Async / await syntax: a newer syntax that avoids having to nest your code.
  • How to create your own promises using the Promise constructor function. 
  • The three promise states: pending, fulfilled, and rejected.
  • Promise static methods: resolve, reject, any, race, all, and allSettled.
  • How callback functions work with asynchronous code (i.e., the old way without promises).


Definitions and the Event Loop

  • First, let's go through some terms and how asynchronous functions flow. We'll start with the JavaScript Runtime Environment which includes the JavaScript engine, Web APIs, and the event loop. In the Node.js environment the Web APIs are replaced with Node Modules and npm packages.
  • There is a separate tutorial on the JavaScript event loop so we will just cover it quickly here.

  • Both the browser and Node.js runtime environments include a JavaScript engine that executes your JavaScript code. Below is a diagram of the JavaScript browser runtime environment.


  • Core JavaScript: consists of the standard constructs of a programming language including setting the syntax and operators, working with data types, constructs like if statements and loops, and global objects such as String, Number, Boolean, Function, Object, Array, etc. Officially it is called ECMAScript. It excludes Web APIs.
  • JavaScript engine: Browsers and Node.js have a JavaScript engine that parses and executes core JavaScript code.
  • Single treaded: The JavaScript engine is single threaded. That means only one statement can be executed at a time. 
  • Synchronous code: Core JavaScript code is executed synchronously meaning the script gets executed one line at a time, waiting for each statement to complete before moving on to the next.
  • API: stands for Application Programming Interface. It is an interface for computer programs to communicate with each other. 
  • Input/Output APIs: To interact with programs outside the JavaScript engine from your script you need to access I/O APIs including Web APIs in the browser or Node modules in Node.js. The browser has many Web APIs with methods for interacting with resources on the web. Node.js has modules with interfaces for interacting with the computer's file system, creating a web server, and more. Some of these APIs execute synchronously and others execute asynchronously.
  • Asynchronous code: When JavaScript does not wait for the result of a function call before moving on to the next line, the function is asynchronous. When the function is called the API goes off and works on the task in the separate program (i.e., outside the JavaScript engine). When the task is complete, the program passes the result and a callback function back to the JavaScript engine to be executed. Some Web APIs and Node modules are synchronous and some are asynchronous. 
  • Callback function: A callback is a function that gets invoked after another function has finished executing. 
  • Handler function: A type of callback function that gets invoked when an event is triggered. Such as a function that gets invoked when a button is clicked.
  • If you want to pause your script and wait until the API result is returned you can use Promises.


Introducing Promises

  • In ancient times, calls to asynchronous functions were managed using callback functions. While this does work it can be confusing, and if there are nested callbacks you may find yourself in what was dubbed "callback hell" with messy, nested code.
  • To solve this, Promises were added to JavaScript as part of ES6 in 2015.
  • A Promise is an object representing the eventual completion or failure of an asynchronous operation. 
  • Many JavaScript asynchronous functions already use promises. 
  • Since you are far more likely to consume already-created promises than to create your own, we will start by explaining how to consume promises. 


Consuming Promises

  • Many asynchronous Web APIs and npm packages now use promises.
  • To tell if a function uses promises you can read the documentation, or you can call it, then test if the result is an instance of Promise.
let res = fetch('https://www.example.com');
const isProm = res instanceof Promise; // returns true

  • We can see from the above that the fetch function returns a Promise object. 
  • Fetch is a Web API that can fetch resources over a network including making GET and POST requests over the internet to a web server.
  • Because fetch is asynchronous, JavaScript will immediately go to the next statement after calling the function. 
  • To get it to wait until a result is returned there are two formats we can use, thenables and async / await. Thenables is the original syntax when Promises were released in 2015. The async / await syntax came out two years later with ES2017.


Thenables
  • Thenables are the collective name for the instance methods that handle promise results: then, catch and finally.

Format:
func() // Call the function that returns a promise
  .then((value) => { handle resolved; })
  .catch((err) => { handle rejected; })
  .finally(() => { optional final method; });

  • The format starts with the call to a function that returns a promise. The initial return value is a Promise object with status pending. 
  • When the function finishes its task it "settles". If the task is successful it is "fulfilled" and returns a value. If it is not successful it is "rejected" and returns a rejection reason.

  • Chain the instance methods to the function call. The instance methods each take a handler function as the argument. Once the promise settles, the instance methods get invoked in order.
  • The then method gets invoked if the promise is successfully fulfilled. It is skipped if the promise is rejected.
  • The catch method gets invoked if the promise is rejected. It is skipped if the promise is successfully fulfilled.
  • The optional finally method gets invoked if the promise is settled (whether fulfilled or rejected). 

Example: Fetch with thenables
let isLoading = true;
console.log(1, 'isLoading:', isLoading);

fetch('https://jsonplaceholder.typicode.com/users')
  .then((res) => { return res.json(); })
  .then((data) => { console.log(2, data); })
  .catch((err) => { console.error(2, err.toString()); })
  .finally(() => console.log(3, 'isLoading:', isLoading = false));
console.log(4, 'Last statement.');

  • Executing the above code will log:
1 'isLoading:' true
4 'Last statement.'
2 [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
3 'isLoading:' false

  • The first statement above sets the isLoading variable to true, then logs it.
  • Call the fetch function: fetch('url')
    • Fetch takes a URL as the argument. The URL we are using is a real website for testing APIs.
    • Fetch is an asynchronous Web API. It makes a GET request to the URL provided and waits for the response.
    • JavaScript does not wait for the result before going to the next line. It skips over the chained thenables, goes to the last line and logs: 4 Last statement., then the script stops.

  • When the HTTP response comes back with data from the server, the Fetch API resolves the promise and passes the response object to the then method.
  • First then method: .then((res) => { return res.json(); })
    • The first then method has a handler function as its argument to handle the response. It takes the HTTP response object as its argument.
    • The HTTP response object contains several properties including the body property containing the requested data in JSON format. 
    • The handler function calls the .json() method on the response object. 
      • .json() is part of the Fetch API. 
      • Like fetch(), it is asynchronous and returns a promise.
      • It extracts the data property from the response object and converts it from JSON format to JavaScript. Then resolves the promise, passing the JavaScript data to the next then method.
  • Second then method: .then((data) => { console.log(2, data); })
    • The handler function in the second then method takes in the JavaScript data as its argument and logs it to the console.
  • Catch method: .catch((err) => { console.error(2, err.toString()); })
    • If there was a problem fetching the data then the promise is rejected and the catch method is called. In the callback we just log the error to the console.
  • Finally method: .finally(() => console.log(3, 'isLoading:', isLoading = false));
    • In the finally method we set isLoading to false and log it.



Async / await syntax
  • The thenable syntax can still get messy with multiple nested Promises.
  • The async and await syntax for handling Promise results was introduced as part of ES2017 as an alternative to thenables.

Format:
async function parentFunction() {
  try {
    const res = await asyncFunc();
  } catch (err) {
    // handle error;
  }
}

  • Adding the async keyword to a function declaration allows you to use the await operator within the function body. 
  • When you place the await operator in front of a new promise object or a function that returns a new promise object with status pending. Your code will pause until the promise is "settled". When the function completes its task: 
    • If successful it returns the resolved promise value which you can set to a variable.
    • If not, it rejects passing a reason and throwing an error.
  • Put the function call in a try / catch block if there is the possibility that the Promise will be rejected.

Example: Fetch with async / await
const parentFunction = async () => {
  let isLoading = true;
  console.log(1, 'isLoading:', isLoading);

  try {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!res.ok) throw new Error(`${res.status}`);
    const data = await res.json();
    console.log(2, data);
  } catch (err) {
    console.error(2, err.toString());
  }

  console.log(3, 'isLoading:', isLoading = false);
});
parentFunction();

  • Executing the above code will log:
1 'isLoading:' true
2 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
3 'isLoading:' false

  • The above example does almost the same thing as the previous fetch example that used thenables. 
    • Except this time we added a line that throws an error if the response object's ok property is false: if (!res.ok) throw new Error(`${res.status}`);
    • If the URL's domain is valid it will return a response object and the Promise will resolve. If the path is invalid or if there is a server error it will still return a response but the response object's "ok" property will be false. For our purposes we want to treat responses that are not "ok" as errors.

Fetch with async await - Reject
  • If the promise is rejected, it gets caught in the try catch block and we log it to the console. 
  • To see a rejected promise in the code, change the fetch url to:
    • 'https://jsonplaceholder.typicode.com/bad-path'
    • or 'http://no-such-url.com' 
    • or disconnect from the internet when running fetch.

  • Then when you execute the code it will log:
1 'isLoading:' true
2 'TypeError: Failed to fetch'
3 'isLoading:' false


Create Promises

  • The rest of this tutorial shows how to create your own Promises, and at the end we will show the old way of dealing with asynchronous functions using callbacks. 
  • Creating your own Promises or asynchronous callbacks is not something you are likely to do often or even at all. It's already been done for most needs, so all you have to do is install the appropriate npm package, or use the appropriate Web API and consume them with thenables or async / await. Or just use callbacks. So continuing with the rest of this tutorial is optional. 
  • However, you may find it useful to understand how Promises work behind the scenes, or you may want to convert an npm package that uses callbacks to Promises. 


Create Promise objects

  • There are asynchronous Web APIs and npm packages that return callback functions instead of Promises. They work fine but if you want to pause your script until the result is returned you can add promises to them. 
  • The format to create a new promise object is:
const prom = new Promise((resolve, reject) => {
  // Perform a task. Return a value if the task is successful or a reason if not.
  if (success) resolve(value);
  else reject(reason);
});

  • Parameter - the Executor function: The parameter for the Promise constructor function is the executor function.
    • The executor function includes the statements to perform some task. If it is successful it resolves to a value. If not it gives a reason.
    • The executor function passes in two built-in functions as parameters: 
      1. The resolve function. Call this function when the function task is successfully fulfilled, passing in the resulting value as the argument: resolve(value);
      2. The optional reject function. Call this function if the task is unsuccessful, passing in a reject reason as the argument: reject(reason); 

  • Let's demonstrate this with an actual asynchronous function, setTimeout. This is available both as a Web API and a Node.js module. It is as basic of an asynchronous function as there is. All it does is count down a timer then return a callback function to execute. You provide the callback function and the duration to count down in milliseconds.
  • To make it slightly more tangible we will simulate querying a database. In web applications querying a database from the server is a common asynchronous task. The process is managed by npm packages such as Sequelize for relational databases and Mongoose for MongoDB. 
  • Our setTimeout simulated database will return a set of usernames, as if it was pulled from a database, or an error message if there was a problem. 

Example - setTimeout using promises - simulates a database query:
const simulateDbQuery = () => {
  return new Promise((resolve, reject) => {
    setTimeout(function processQuery() { console.log('Processing the database query'); const success = Math.round(Math.random()) === 1; // Randomly returns true or false
      if (success) {
        const data = ['Joey', 'Sheena', 'Johnny', 'Judy'];
        resolve(data);
      } else {
        const reason = 'There was a problem';
        reject(reason);
      }
    }, 2000);
  });
};

  • When the above simulateDbQuery function is called it takes no arguments and returns a new Promise object. 
  • The Promise constructor function takes in the executor function as its argument.
  • Our executor function contains the setTimeout Web API. 
  • SetTimeout: The setTimeout takes two arguments:
    1. The callback function which we named processQuery that executes after the timer delay expires.
    2. The timer delay of 2000 milliseconds (i.e., 2 seconds).
  • The ProcessQuery function: simulates our database query.
    • The first line simulates a database query by logging: "Processing the database query"
    • The second line randomly determines if the query was successful or not. The Math.round(Math.random()) === 1 formula randomly sets the success variable to either true or false. 
    • The if statement checks if success is true. If so it calls the resolve function passing in an array of usernames: resolve(data). This gets returned as the fulfilled promise value.
    • If success is false it calls the reject function passing in a reason as the argument: reject(reason)


Handle Example Result with Thenables:
[9:12 Video Timestamp]

  • Now that we have a function that returns a promise, we can consume the promise the same way we did with the Fetch API, using either the thenables or async / await syntax. We'll start with thenables.

console.log(1, 'First statement');

simulateDbQuery()
  .then((data) => { console.log(2, data); })
  .catch((err) => { console.error(2, err); })
  .finally(() => { console.log(3, 'Promise has settled.'); });
console.log(4, 'Last statement'); 

  • Executing the above code, if successful will log:
1 First statement
4 Last statement
Processing the database query
2 ['Joey', 'Sheena', 'Johnny', 'Judy']
3 Promise has settled.
  • If unsuccessful will log:
1 First statement
4 Last statement
Processing the database query
2 There was a problem.
3 Promise has settled.

  • The first statement logs: 1 First statement
  • The simulateDbQuery function is called and returns a pending promise. The methods chained to it are not invoked until the promise settles.
  • The last statement logs: 4 Last statement. The script is now done and is waiting for the promise to be settled.

  • The browser timer counts down the 2 second delay which we are pretending is the database is being queried. When the timer finishes:
    • If successful the then method is invoked. The promise returns a value (the username array) which gets passed into the then method's handler function which logs it to the console:
      • .then((data) => { console.log(2, data); }) 
      • logs: 2 ['Joey', 'Sheena', 'Johnny', 'Judy']
    • If not successful the promise is rejected. The catch method gets invoked: 
      • .catch((err) => { console.error(2, err); }) 
      • logs: 2 There was a problem
    • Then the finally method gets invoked:
      • .finally(() => { console.log(3, 'Promise has settled.'); });
      • logs: 3 Promise has settled.

Handle Example Result with async / await:
[12:27 Video Timestamp]

btn2.addEventListener('click', async function eventHandler() {
  console.log(1, 'First statement.');

  try {
    const data = await simulateDbQuery();
    console.log(2, data);
  } catch (err) {
    console.error(2, err);
  }

  console.log(3, 'Last statement.');
});

  • Executing the above code, if successful will log:
1 First statement
Processing the database query
2 ['Joey', 'Sheena', 'Johnny', 'Judy']
3 Last statement.
  • If unsuccessful will log:
1 First statement
Processing the database query
2 There was a problem.
3 Last statement.

  • To use await before a function returning a promise, it must be inside an async function. So we added an event listener to the btn2 element, listening for the click event. When clicked we invoke the eventHandler function and make it an async function. 
  • The first statement in eventHandler logs: 1 First statement.
  • Then we call simulateDbQuery with await in front of it. That will pause our code until the promise is settled. 
  • The function counts down the timer, executes its function, and settles the promise either returning a fulfilled value or rejecting it which throws an error.
  • If successful the fulfilled value gets assigned to the variable data, which gets logged to the console.
  • If rejected, an error is thrown and caught in the catch block. We log the reject reason to the console.
  • Then the last line logs: 3 Last statement.

Node.js modules
  • Node.js has both synchronous and asynchronous modules.
  • And some modules have both synchronous and asynchronous versions of the same function, particularly in the fs file system module. For example there are two methods that return the text of a file.
    • fs.readFile(path[, options], callback) is an asynchronous method that returns the file text in a callback.
    • fs.readFileSync(path[, options]) is a synchronous method that pauses the script until the file text is returned.
  • And Node has a utility called util.promisify() that adds promises to asynchronous functions. This is out of scope for this tutorial.


Promise States

  • Promises are objects and objects have properties. 
  • Internal properties are used internally by JavaScript, but can't be accessed by your code. You can see them in Chrome Dev Tools though. They are surrounded by double square brackets.
  • Promises have two internal properties, PromiseState and PromiseResult.
  • PromiseState has 3 potential values. 
    • Pending means the the promise object was created, but has not settled, so it doesn't have a result yet.
    • If the resolve function is called, PromiseState changes to fulfilled.
    • If the reject function is called, PromiseState changes to rejected. 
  • The PromiseResult value is undefined while it is pending. And when settled it changes to the resolve value or reject reason.

  • You can view the promise object in Chrome Dev Tools. Using the simulatedDbQuery function as example, call the simulateDbQuery function without the await keyword setting it to a variable we'll call prom. Log prom to the console. Then apply the await keyword. That way you can view the promise object when it is first returned, and when it is settled.
btn3.addEventListener('click', async function eventHandler() {
  let prom = simulateDbQuery();
  console.log(1, prom);
  data = await prom;
  console.log(2, data);
});

  • When the function is called it initially returns a promise object with internal properties:
1 Promise {
  [[PromiseState]]: 'pending', 
  [[PromiseResult]]: undefined
}

  • Two seconds later when the promise is settled, if it is fulfilled the promise object internal properties are:
1 Promise {
  [[PromiseState]]: "fulfilled", 
  [[PromiseResult]]: ['Joey', 'Sheena', 'Johnny', 'Judy']
}
  • Or if it is rejected the promise object internal properties are:
1 Promise {
  [[PromiseState]]: "rejected", 
  [[PromiseResult]]: "There was a problem"
}


immediately Resolved Promises
  • If you wrap a synchronous function in a promise object it will return the settled promise immediately, skipping the pending step.



Static Methods

  • Static methods are methods that you call on the class instead of on an instance of the class. There are six Promise static methods (length, name, and prototype are properties).
Object.getOwnPropertyNames(Promise); // returns: length, name, prototype, all, allSettled, any, race, resolve, reject

  • You may never have a need to use these methods but we will demonstrate simple examples of each anyway.


Static methods - resolve, reject


  • We will start with resolve and reject.
  • We saw earlier how to instantiate a new Promise object, with an executor function as the argument and resolve and reject functions as arguments within the executor.

  • There are also static Promise resolve and reject methods that return a new Promise object already fulfilled or rejected with the given value. 

Format:
Promise.resolve(value); // Returns a new Promise object, resolved with the given value.
Promise.reject(reason); // Returns a new Promise object, rejected with the given reason.

Example:
btn.addEventListener('click', async function eventHandler() {
  const res = await Promise.resolve('This is a resolved promise value.');
  console.log(1, res); // logs: 1 'This is a resolved promise value.'
});
  • Immediately returns the resolved promise. You still need to use async / await or thenables.

btn2.addEventListener('click', async function eventHandler() {
  try {
    await Promise.reject(new Error('This promise was rejected.'));
  } catch (err) {
    console.error(2, err.toString()); // logs: 2 'Error: This promise was rejected.'
  }
});
  • Immediately returns the rejected promise and throws an error. Use a try...catch statement to catch the error.


Static Methods - any, race, all, allSettled

  • The any, race, all, and allSettled methods deal with running multiple promises concurrently. Each of these methods takes an array of promise objects as the argument.

Example:
  • Below are three Promise constructors that settle after 1, 2, and 3 seconds respectively. The first is rejected and the other two are fulfilled.
// Prom 1 returns a rejected promise after 1 second
const prom1 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('1 Rejected after 1 sec')), 1000);
}); // Prom2 returns a resolved promise after 2 seconds
const prom2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('2 Resolved after 2 secs'), 2000);
}); // Prom3 returns another resolved promise after 3 seconds
const prom3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('3 Resolved after 3 secs'), 3000);
});

  • You can apply the below static functions, passing in the three promise objects as the argument.

  • Promise.any([prom1, prom2[,...]]) // Returns the resolved value from the first promise that gets fulfilled.
const res = await Promise.any([prom1, prom2, prom3]); 
console.log(res); // logs '2 Resolved after 2 secs'

  • Promise.race([prom1, prom2[,...]]) // Returns the resolved value or rejected reason from the first promise that settles.
try {
  const res = await Promise.race([prom1, prom2, prom3]);
  console.log(res);
} catch (err) {
  console.error(err); // logs 'Error: 1 Rejected after 1 sec'
}

  • Promise.all([prom1, prom2[,...]]) // Returns a promise that resolves when all promises resolve, returning an array of the resolved promise values. But if any promise rejects, then the whole thing rejects.
try {
  const res = await Promise.race([prom1, prom2, prom3]);
  console.log(res);
} catch (err) {
  console.error(err); // logs 'Error: 1 Rejected after 1 sec'
}

  • Promise.allSettled([prom1, prom2[,...]]) // Returns a promise that resolves when all promises have settled. It returns an array of objects for all the promises, whether fulfilled or rejected. Each object has properties for status and value if it was fulfilled, or reason if it was rejected.
const res = await Promise.allSettled([prom1, prom2, prom3]);
console.log(res); // logs array promise objects

Asynchronous Functions - with Callbacks

  • The last topic we will cover is handling asynchronous functions using callbacks. This is how asynchronous functions were handled before Promises were introduced. Callbacks are still commonly used so it is useful to know how they work. They work perfectly fine, they are just messier to work with than Promises if there are multiple nested callbacks (i.e., callbacks within callbacks).

  • You pass the callback function in as an argument to the API call.

Format:
const asyncFunc = (callback) => {
  const result = /* do some task that returns a result */
  return callback(result); }; const callback = (result) => /* do some operation on the result */;
asyncFunc(callback); //Call an asynchronous function passing in a callback.

  • We will explain this using an example. We'll use the simulateDbQuery function from our earlier examples, which simulates querying a database. 
  • Instead of using promises, we'll pass in and then return a callback function. Below is the code:

Example - simulate a database query with setTimeout:
// Asynchronous function
const simulateDbQuery = (dbCallback) => {
  setTimeout(function processQuery() { console.log('database processes the query and returns the data');
    const data = ['Joey', 'Sheena', 'Johnny', 'Judy'];
    return dbCallback(data);
  }, 2000);
};
// Callback function
const dbCallback = (data) => {
  console.log(data);
}
// Function call
simulateDbQuery(dbCallback); console.log('Last statement.');

  • The three components of this process are:
    • The asynchronous function simulateDbQuery that queries the simulated database. It has one parameter for the callback function. 
    • The callback function: dbCallback. It takes the query result data as the parameter and then logs it as item 2 to the console.
    • The asynchronous function call, which passes in the callback as the argument. 

  • When you execute the code it takes the following steps:
    • It calls the simulateDbQuery function, passing in the dbCallback function as its argument.
    • The simulateDbQuery function calls the setTimeout Web API and the browser begins counting down the timer in the background. 
    • The JavaScript engine moves on to the next statement in our script which logs: "Last statement." The script is done.

  • When the setTimeout Web API finishes counting down the 2 second delay it invokes the function in the first argument: processQuery.
  • ProcessQuery simulates the actual database query. It returns an array of names assigned to the variable data.
  • This is where the callback function comes into play. The return value of the asynchronous function (i.e., processQuery) is the callback function dbCallback. It takes the value returned from the API as the argument which is the array of user names. Then handles it back in our script. In this case it just logs the data to the console.

  • And that concludes this tutorial on JavaScript Promises.



Conclusion

The topics in this tutorial correspond with the JavaScript CheatSheet Promises category. Make sure you understand how to use existing promises. Learning how to create your own promises and using Promise static methods is optional.
Open CheatSheet Back To List Next Tutorial