WARNING! This is an advanced topic which is aimed at those of you finding the unit, and programming in general, relatively straightforward. You should only attempt this if you are confident programming functions, callbacks, and passing arguments to functions, and have completed all previous exercises from the majority of previous topics (Topics 1-5, 6-8 and 11-12) and are confident with your answers.If you are struggling with the unit you will struggle immensely with this topic, so it is recommended that you catch up with previous topics first. You can then return to this topic later, when you are thoroughly confident with previous topics and working with functions and callbacks.
A promise is an object which "promises" to do a particular (typically) asynchronous, background task which will complete at some point in the future. An AJAX request is a typical example; promises are used extensively in AJAX development. One of two functions are called depending on whether it fulfilled that "promise":
Promises allow you to write code with intuitive, clean syntax in this form:
promise.then(resolveFunction).catch(rejectFunction);where "promise" is the promise object. What this code means is:
Perform the task associated with a promise, then run a function (resolveFunction in this example) which runs if the promise is fulfilled, and catch any error conditions with an error handling function (rejectFunction in this example).Hopefully you can see that this code is cleaner than the typical callback-based AJAX code. In general, Promises allow us to write code for asynchronous processes (i.e. callback based processes, where a function runs at some future point) in a cleaner and more sequential way. This is because the "resolve" function will only run once the asynchronous task (e.g. AJAX call) has completed.
Promises produce cleaner code; however, learning how to write a promise in the first place is conceptually harder than the "classic" approach to asynchronous tasks such as AJAX. However, using pre-written Promises is easy, so once you've written your promise, coding does become easier!
A promise has an associated task, which is a background task which may either succeed (fulfilling the promise) or fail (rejecting the promise). This task is implemented as a function which must be passed into the Promise constructor as a parameter. This task function must take two parameters, each of which is themselves a callback function, and each of which was described above:
The task function must then call these two functions as appropriate, i.e. if the task succeeds, it must call the resolve function and if it fails, it must call the reject function.
We are going to start by writing some promises to perform simple mathematical operations, and then move onto a more "real world" example in the form of a promise-based AJAX call.
Here is an example of a promise to perform a division of two numbers. The promise is fulfilled with the result of the division, or rejects if the denominator is zero (remember that division by zero is not allowed). You can write this on Neptune as node.js code, or in the browser.
function divide(a,b) { return new Promise ( (resolve, reject) => { if(b == 0) { reject("Cannot divide by zero!"); } else { resolve(a/b); } }); }Note what we are doing here is writing a function called divide() which divides two numbers and returns a Promise to do the division. Note how the Promise constructor takes, as an argument, a task function which attempts to carry out the promise. This task function gets passed two parameters, a resolve function and a reject function, as we saw above. The task function then tests if the denominator (b)is zero, and if it is, the promise is rejected with a message "Cannot divide by zero!". Otherwise, the promise is fulfilled and the resolve function is called, with the result of the division.
Calling the promise
To call the promise, we call the function which returns the promise and chain it with a then() call. then() takes a function as an argument; this function will run as soon as the promise is fulfilled. This function corresponds to the resolve parameter to the Promise constructor. So whatever is passed to then() will become the resolve parameter of the Promise.
divide(18, 2).then( result => { console.log(result); } );
To handle a promise rejection (e.g. division by zero in the example above), we chain a catch() to our promise call, e.g:
divide(18, 0).then( result => { console.log(result); } ).catch(err => { console.log(err); } );Note how catch() also takes a function as an argument; this function corresponds to the reject parameter of the Promise constructor. So whatever is passed to catch() will become the reject parameter of the promise.
Promises are frequently chained together. We perform one operation which returns a promise, and then if the promise fulfils, we can call another function, using this form of code:
function1.then(function2).then(function3).catch(e => { console.log(`Error: ${e}`); }););
What's this doing? We're calling function 1, which returns a promise. If the promise fulfils, then we call function2, which also returns a promise. If that promise fulfils, we call function 3. But if any of the promises reject, then the catch function will run and report the error associated with the rejection.
A key, important point about promise chaining is that the value associated with a promise is passed as an argument to the resolve function. For example
function divide(a,b) { return new Promise ( (resolve, reject) => { if(b == 0) { reject("Cannot divide by zero!"); } else { resolve(a/b); } }); } function squareroot(n) { return new Promise( (resolve, reject) => { if(n < 0) { reject("Cannot square-root a negative number!"); } else { resolve(Math.sqrt(n)); } }); } function countTo(n) { return new Promise( (resolve, reject) => { if(n <= 0) { reject("Cannot count to a number less than 1!"); } else { for(let i=1; i<=n; i++) { console.log(i); } } });We can chain these as follows:
divide(100,4).then(squareroot).then(countTo).catch(e => { console.log(e); });What happens here, exactly?
Here is a further, more complex example of using promises. In this example we develop a Promise to perform an AJAX request. If the AJAX request succeeds (i.e. it returns an HTTP code not in the 400-599 range) we call our resolve function and pass it the response text returned from the server. If on the other hand, the HTTP code returned is in the 400-599 (error) range, we reject the promise with the error code.
Here is an example of such a Promise associated with an AJAX request.
function ajaxPromise(url) { return new Promise( (resolve,reject)=> // this is the task function { var xhr2 = new XMLHttpRequest(); xhr2.open('GET', url); xhr2.addEventListener("load", (e)=> { if(e.target.status>=400 && e.target.status<=599) { // pass the HTTP code to the reject function reject(e.target.status); } else { // pass the response text to resolve function resolve(e.target.responseText); } } ); xhr2.send(); } ); }
To use our promise-based AJAX request:
ajaxPromise('/artist/Oasis').then(ajaxSuccess).catch(handleError);where ajaxSuccess is a function which processes a successful AJAX request and handleError() is a function which processes error conditions. The arguments to the resolve and reject functions would then be passed to ajaxSuccess() and handleError(), respectively:
function ajaxSuccess(responseText) { var data = JSON.parse(responseText); // do something with the data.. } function handleError(error) { alert('Error: ' + error); }
We could also use promise chains in AJAX. For example if the AJAX promise is fulfilled, we could then perform another promise to parse the JSON returned.
ajaxPromise('/artist/Oasis').then(parseJson).then(showJsonResults).catch(handleError)How might this work? The idea is that parseJson() itself returns a further promise. It might look like this:
function parseJson(responseText) { return new Promise ( (resolve,reject) => { var parsedData = JSON.parse(responseText); if(parsedData.length==0) { reject("No matching results!"); } else { resolve(parsedData); } } ); }
showJsonResults() would then receive the parsedData in the above example, because showJsonResults is the resolve function passed to the then() of the promise returned by parseJson(). As parsedData() is the result of JSON.parse(), it will be (typically) an array of objects so it can be processed in the usual way:
function showJsonResults(data) { var html = ""; data.forEach ( flight=> { html = html + `Depart ${flight.depart} Arrive ${flight.arrive} <br />`; }); document.getElementById("results").innerHTML = html; }
However, we can make it even simpler. We can rewrite our parseJson() in this way and the promise chaining will still take place:
function parseJson(responseText) { return JSON.parse(responseText); }Why will this work, when parseJson() doesn't return a Promise? The reason is that if a function which does NOT return a Promise is passed to then(), a new Promise object will automatically be created, wrapping the data returned by the function.
A reminder of the chain:
ajaxPromise('webservice.php').then(parseJson).then(showJsonResults)The first then returns a Promise to run parseJson(). If parseJson() runs successfully (it always will here), then a promise will be created, it will call showJsonResults() as its resolve function and the return value of parseJson() will be passed as an argument to showJsonResults().
As a result of this, we can completely remove our parseJson() function altogether and simply pass JSON.parse in as the argument to the first then():
ajaxPromise('webservice.php').then(JSON.parse).then(showJsonResults)Why? JSON.parse() returns the parsed JSON, and a promise will be created automatically (as before), wrapping the parsed JSON, which will be fulfilled with the argument to the next then(), namely showJsonResults(). Consequently, showJsonResults() will receive the return value of JSON.parse().
Make a copy of your AJAX search script from Topic 6 (the basic version, not the version using DOM) and change it so that the AJAX is performed using promises.
Above we saw how to do AJAX requests using promises. However, standards-compliant browsers (such as Firefox and Chrome) now have a built in API - the fetch API- which uses promises. You can use the fetch API without having to write an AJAX promise yourself. Here is a simple example:
fetch('http://www.free-map.org.uk').then(response => response.text()).then(console.log);fetch() returns a promise to return the response from the given URL. The resolve function of this promise takes, as a parameter, a response object. This is not the same as a traditional XMLHttpRequest, but does have a couple of interesting methods:
fetch('https://example.com/api/artist/Oasis').then(response => response.text()).then(showJsonResults); function showJsonResults(text) { var data = JSON.parse(text); var html = ""; data.forEach ( flight=> { html = html + `Depart ${flight.depart} Arrive ${flight.arrive} <br />`; }); document.getElementById("results").innerHTML = html; }
We can make this easier though with the response.json() promise, which promises to parse the JSON returned and resolves with the parsed JSON, e.g:
fetch('https://example.com/api/artist/Oasis').then(response => response.json()).then(showJsonResults); function showJsonResults(data) { var html = ""; data.forEach ( flight=> { html = html + `Depart ${flight.depart} Arrive ${flight.arrive} <br />`; }); document.getElementById("results").innerHTML = html; }
One advantage of the fetch API is that you can easily send not only GET and POST requests but also PUT and DELETE. Here are some examples of each method:
var flight = { depart: '0900', arrive: '1200', price: 100.0, flightnumber: 'SA101', destination: 'Rome' }; fetch('/flight/create', { method: 'POST', body: JSON.stringify(flight), headers: {'Content-Type': 'application/json' } }).then(...);This will send a POST request to the route /flight/create. Note how we send the POST data here. Rather than sending post fields via a FormData object as described in Topic 7, we populate the body of the HTTP request with JSON representing the flight. Note also how we have to set the Content-Type header appropriately.
We can read the JSON back using Slim in a similar way to regular POST fields. The getParsedBody() method:
$req->getParsedBody();will return an associative array containing the parsed JSON.
DELETE requests are easy:
fetch('/flight/9818', { method: 'DELETE'}).then(...);
PUT follows a similar pattern to POST. JSON is sent to the server within the body of the request. On the server side (Slim), the PUT data is read in the same way as PUT data sent over cURL (see topic 5):
var flight = { depart: '0930', arrive: '1230'}; fetch('/flight/9819', { method: 'PUT', body: JSON.stringify(flight), headers: {'Content-Type': 'application/json' } }).then(...);
Make a further copy of your AJAX exercise (Topic 6) and change it to use the fetch API. Again, it should be the basic version, not the version using DOM
You can use a third-party implementation of the fetch API to perform cURL-like requests from a Node app. Install node-fetch:
sudo npm install -g node-fetchYou can then write code such as:
const fetch = require('node-fetch'); fetch('http://www.free-map.org.uk').then(response => response.text()).then(console.log);
ECMAScript 8 (aka ECMAScript 2017) introduces async/await. This allows you to write promise-based, asynchronous code in a sequential manner. Here is how we would rewrite our own custom Promise-based AJAX code (i.e. not using the fetch API) using async/await:
async function ajaxrequest() { var a = document.getElementById("origin").value; var b = document.getElementById("destination").value; var url = `/artist/Oasis`; var responseText = await ajaxPromise(url); var data = JSON.parse(responseText); showJsonResults(data); }Note how we're doing a whole AJAX / JSON parsing procedure using sequential code, even though AJAX is an asynchronous process. The key things are:
A number of articles were used to draw up these notes, which are also useful further reading. These include: