Promises and the Fetch API
Introducing Promises
- Promises are used in asynchronous programming to receive the eventual value (if successful) or reason for failure (if unsuccessful) of the asynchronous call.
- As the name suggests, it is a promise to eventually return something.
- Since everything in JavaScript is an object, it should be no surprise that Promise objects get returned.
- The Promise object, while in process, has a
pendingstatus.- If it succeeds and returns a value, the status is
fulfilled. - If it failed, the status is
rejected.
- If it succeeds and returns a value, the status is
- Promise objects also have
then(),catch(), andfinally()methods.then()is called when the Promise object's status isfulfilledorrejected(when it is settled, for better or worse), so that triggers whatever code is associated withthen()- This is a very important method, because it allows you to delay actions until data has returned (the retrieval time will vary depending on connection speed, network conditions, browser used, server response time, etc.).
- It also allows for promise chaining, in which you can string together a series of
then()statements, which could (at your discretion) be a series of promises, resolved one at a time in order. - The
catch()method is often the last step, after a series ofthen()statements, and it is reserved for error handling / error reporting. - Note that it's possible for a
then()to follow acatch(). - As desired,
finally()can also be specified as the very last step. This gets invoked once the Promise is settled, regardless of the final status.
- XHR calls do not create Promise objects, so in order to chain together a sequence of actions (such as a series of calls in sequence), you might end up writing convoluted nested code, as the response to one XHR call triggers another, then another, etc. This has been referred to as the pyramid of doom, as the nested code begins to resemble an upside-down pyramid, which resembles something like:
xhrCall1(function() { xhrCall2(function() { xhrCall3(function() { xhrCall4(function() { xhrCall5(function() { // finally done! }); }); }); }); });
Fetch API Fundamentals
- The Fetch API is a newer approach to asynchronous programming that generates Promise objects (technically it's a Stream object, but once we start accessing the data stream it returns a Promise object).
- Unlike XHR, which does have a synchronous option, Fetch is always asynchronous.
- Another difference with XHR is what Fetch considers to be a rejection.
- With XHR, failure occurred if the file could not be found (404 HTTP status code returned) or the response was a fatal server error (500 HTTP status code returned).
- A fetch call has different criteria for rejection (failure). If the network failed that would do it.
- In order to detect a 404 or 500, one could check the
statusproperty of the response and look for anything above 299 (or check for 200, which is success). There is also anokboolean property on the response, so if that isfalsethen the status was greater than 299.
- When a Fetch call is made, all that is required is a URL.
- An optional Object can also be passed as the second parameter that sets other options, such as POST data, headers, caching instructions, etc.
- The basic structure of a Fetch call:
fetch('https://www.example.com/', { method: 'GET' }).then(function(response) { // successfully resolved, so proceed with other actions }).catch(function(error) { // log the error or take some other action as a result }); - One can also provide
finally():fetch('https://www.example.com/', { method: 'GET' }).then(function(response) { // successfully resolved, so proceed with other actions }).catch(function(error) { // log the error or take some other action as a result }).finally(function(response) { // this occurs once the Promise is settled, and regardless of success/failure }); - There are a number of options to possibly set; MDN has a good overview of the various Fetch API option possibilities.
- It is also important to be aware of the properties and methods of the Fetch API response.
Fetch Code Example
- This code is a rewrite of the CSV example from one of the XHR articles.
- It is a "live search" that returns matching results from a data set retrieved via Fetch.
- After the user stops typing, a 1.5 second timer counts down and shows the matches (if any exist).
- Use the browser's Developer Tools (press F12 to launch them or right-click the page and choose 'Inspect') and check the Network tab to see the call and response. You may need to reload the page if you open the Developer Tools too late.
Structure (csv_search.html)
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <title>Fetch API CSV Live Search Example</title> <meta charset="utf-8" /> <link rel="stylesheet" href="csv_search.css" /> </head> <body> <form method="post" action="#" role="search"> <p>Search for an Article: <input type="search" id="query" size="20" /> <input type="submit" id="submitButton" value="Search" /></p> </form> <div id="results" aria-live="polite"></div> <script src="csv_search.js"></script> </body> </html>
Presentation (csv_search.css)
body, h1 {
font: 75%/1.3 verdana, sans-serif;
}
h1 {
font-size: 120%;
font-style: italic;
margin-top: 20px;
border-top: 1px solid #000;
padding-top: 20px;
}
input {
border: 1px solid #000;
}
#waiting {
font-size: 80%;
font-weight: bold;
}
Data (searchData_csv.txt)
This CSV data is arranged in pairs, with the title followed by its URL.
AJAX Basics,http://www.ajaxbasics.com,AJAX 123,http://www.ajax123.com,Intro to AJAX,http://www.introajax.com,About AJAX,http://www.aboutajax.com,Learning AJAX,http://www.learningajax.com,AJAX Intro,http://www.ajaxintro.com,Discovering AJAX,http://www.discoveringajax.com,AJAX Foundations,http://www.ajaxfoundations.com
Behavior (csv_search.js)
"use strict";
// create the 'ls' (live search) object
const ls = {
// reference to the text input box
searchTextBox: document.querySelector("#query"),
// reference to the results div
resultsArea: document.querySelector("#results"),
// div that holds the waiting message
waitingHolder: document.createElement("div"),
// reference to the form element node
theForm: document.querySelector("form"),
// initially there is no waiting message
waitMsg: false,
// initially there is no timer set
timer: null,
// this will hold the CSV data
searchData: null,
// flag for tracking if results were found
resultsFound: false,
init: function () {
// disable submit button; will appear if JS is disabled
document.querySelector("#submitButton").style.display = "none";
// on every key stroke call the setTimer function
this.searchTextBox.addEventListener("keyup", this.setTimer, false);
// send the request for the CSV data
// first then() ensures we have data (it only executes when promise fulfilled)
// response.text() returns another promise that resolves to a UTF-8 decoded string
// second then() does not execute until the response.text() promise is fulfilled
// second then() splits string into an array and assigns it to searchData property
// if any promise is rejected, catch() is called
fetch("searchData_csv.txt", {
method: "GET",
cache: "no-cache"
})
.then(function (response) {
return response.text();
})
.then(function (response) {
ls.searchData = response.split(",");
})
.catch(function (error) {
console.log(error);
});
},
setTimer: function () {
// if the clock is ticking, clear the clock
if (ls.timer) {
clearTimeout(ls.timer);
}
// if there is not currently a waiting message, display one
if (!ls.waitMsg) {
ls.waitingHolder.id = "waiting";
const theMsg = "Waiting for you to finish entering your search...";
const waitingTxt = document.createTextNode(theMsg);
ls.theForm.appendChild(ls.waitingHolder).appendChild(waitingTxt);
ls.waitMsg = true;
}
// after 1.5 seconds, call the displayResults() function
// this countdown will be reset if the user keeps typing
ls.timer = setTimeout(ls.displayResults, 1500);
},
displayResults: function () {
// shorthands
const data = ls.searchData;
const results = ls.resultsArea;
const box = ls.searchTextBox;
results.innerHTML = "";
// if the user has deleted all the text in the query, do not go any further
// the keypresses involved in deleting the query will trigger the function
if (!box.value) {
return;
}
// determine the total number of possible results for subsequent loops
const totalResults = ls.searchData.length;
// lowercase the query to eliminate mismatches due to case
const queryValue = box.value.toLowerCase();
// wipe out the waiting message and flip the waitMsg flag back to false
ls.waitingHolder.removeChild(ls.waitingHolder.firstChild);
ls.theForm.removeChild(ls.waitingHolder);
ls.waitMsg = false;
// create and append the Search Results header
const searchHeader = document.createElement("h1");
const searchHdrTxt = document.createTextNode("Search Results");
results.appendChild(searchHeader).appendChild(searchHdrTxt);
// no results found yet
ls.resultsFound = false;
// since the array is arranged in sets of two values,
// jump ahead 2 with each loop
for (let i = 0; i < totalResults; i += 2) {
// pull out each article title and lowercase it
const article = data[i].toLowerCase();
// if the query matches the article title
// then continue and construct the result for that item
if (article.match(queryValue)) {
ls.resultsFound = true;
const newLink = document.createElement("a");
newLink.href = data[i + 1];
const linkText = document.createTextNode(data[i]);
results.appendChild(newLink).appendChild(linkText);
const lineBreak = document.createElement("br");
results.appendChild(lineBreak);
}
}
// if flag for results is never flipped from false to true, then no results found
// display the 'No results were found' message
if (!ls.resultsFound) {
results.innerHTML = "No results were found.";
}
}
};
ls.init();
Async and Await
- One of the drawbacks with the approach shown previously is that the code gets complicated and lengthy, as each Promise has a function within
then(),catch(), andfinally(). - An alternative approach uses
asyncandawait, as those return the result of an asynchronous call as a value.asyncis a keyword applied to a function; that keyword being included means the function is returning a Promise and is executing asychronously. Even if what is returned is not identified as a Promise, the return value will be wrapped in a Promise.awaitis another keyword that tells JavaScript to return the Promise results, rather than returning the Promise object.awaitcan only be used withinasyncfunctions; outside of that context an error should be thrown.
- Code following an
awaitwill not execute until thatawaitreturns its value and relinquishes control.
Async and Await Code Example
The relevant JavaScript changes from the prior Fetch example:
init : async function() {
// disable the submit button; will appear if JS is disabled
document.querySelector('#submitButton').style.display = 'none';
// on every key stroke call the setTimer function
this.searchTextBox.addEventListener('keyup', this.setTimer, false);
// send the request for the CSV data
// wrap in a try / catch for error handling
// the first await retrieves the Promise object from the fetch
// the second await then retrieves the Promise object's content as text
try {
const response = await fetch('searchData_csv.txt', { method: 'GET', cache: 'no-cache' });
const data = await response.text();
ls.searchData = data.split(',');
}
catch (error) {
console.log(error);
}
},
See how this Async / Await example renders
Cross-Origin Resource Sharing
- It is common to request data from other sites, such as pulling in data from a third party API.
- Since the request originates on one domain and tries to pull in data from another domain, that is cross-origin resource sharing and JavaScript's same origin policy kicks in to block it.
- For this to work properly the server or script receiving the XHR or Fetch request must be set up to allow the request to work.
- They can either accept requests from a limited set of domains or all domains.
- This is specified in an
Access-Control-Allow-Originheader that it sent.
- For simpler scenarios, like retrieving a file from another server or POSTing data to another server, the header is checked when the request occurs.
- In more complicated scenarios, such as cases where authentication is occurring via a middleware layer, this header is checked in a preflight call that determines support for cross-origin sharing and returns an authentication token (if successful), which is then followed by the XHR or Fetch call if a token was returned. In those instances you will see two entries in the Network activity in your browser's Developer Tools.