This topic will look at the concept of promises. When using AJAX last year, you will have used the fetch() API and may well remember that you have to either
specify a follow-on method to run when the response has been received, with then(), or
await its result.
So for example:
const text = fetch("URL").then(response => response.text());
or:
const response = await fetch("URL");
const text = await response.text();
What is going on here? The
fetch call, above, returns a promise object.
A promise is an object which "promises" to do a particular (typically)
asynchronous, background task which will complete at some point in the future,
such as an AJAX request. Typically a function returns a promise immediately, but the task associated with the promise does not finish immediately. The promise will complete at some point in the future, when we say it has been fulfilled or resolved. For example, the AJAX fetch() function will return immediately, before the server has sent back a responde. It will return a promise, which will be resolved or fulfilled when the server returns a response. You then define a function which will run when the promise is fulfilled using the promise's then() method, e.g.
doSomeLongRunningTask().then(results => processTheResults(results));
Here, doSomeLongRunningTask() returns a promise immediately, but the promise is only fulfilled or resolved at some future point in time. When the promise has been fulfilled, the function passed as an argument to then() will run. Here this is an arrow function which receives the parameter results and then calls processTheResults(), passing it this parameter. The value results is the value returned when the promise has been fulfilled: I will call this the resolve value. For example for an AJAX request the resolve value would be the response from the server.
As we have seen, an example of a long running task would be the AJAX fetch(). Another example might be code to query a database or open a file (though better-sqlite3 does not use promises).
Thus promises allow you to write asynchronous code, by specifying functions that run at some point in the future when some long-running task has completed.
Promises can be fulfilled or rejected. These outcomes are as follows:
then() and
catch() functions. The function supplied as an argument to
then (the resolve function)
will run as soon as the promise is fulfilled. We can pass an argument into this function: as we have seen, this is the resolve value.
Similarly the function supplied as an argument to catch will run as soon as the promise is rejected. We can also pass a value into this function.
So, promises allow you to write code with intuitive, clean syntax in this form:
promise.then(resolveFunction).catch(rejectFunction);where "promise" is the promise object, and
resolveFunction and
rejectFunction are the functions which run on fulfilment and
rejection, respectively.
To implement your own promise, you need to create a Promise object. A Promise object has an associated task, which is a background
task which may either succeed (fulfilling the promise) or fail (rejecting the promise). This task might be an AJAX call, for example, or code to connect to a database or read a file. It is implemented as a function which must be passed into the Promise object as a parameter. This task function must take two parameters, each of which is another function:
resolve function (called if the promise is fulfilled)reject function (called if the promise cannot be fulfilled)Each of these is specified by the code calling the promise.
then() ends up as the resolve function;catch() ends up as the reject
function.
The task function must then call these two functions appropriately.
So, if the task succeeds, it must call the resolve function
and if it fails, it must call the reject function. This allows then and catch code to handle the promise resolving or rejecting, as appropriate.
We are going to illustrate the concept by writing a promise to perform division. As you may know, division by zero is fundamentally invalid. So the promise will resolve if the division was successful, or reject if the denominator (second number) is zero.
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 object, when created with new,
takes 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 passed as an argument. The argument to the resolve function is the promise's resolve value, which we saw above.
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 object. So whatever is passed to then() will
become the resolve parameter of the Promise.
divide(18, 2)
.then( result => console.log(result) );
As we have seen, 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 object. 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.
The thing that makes promise chaining work effectively is the fact that the resolve value of a promise is passed to the resolve function as a parameter. 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?
divide() function returns a
promise, which will be fulfilled with a/b if the denominator is not zero,
and will reject with a "Cannot divide by zero!" message if it is.
divide(). If the promise
succeeds, we run the argument to its then(), i.e. the
squareroot()
function. The squareroot() function will receive the resolve value of the previous promise, i.e. the result of the division, as a parameter.squareroot() then itself returns a promise, with a
resolve function which receives the square root of the number.
This promise will reject if the number is negative (as we cannot
square root negative numbers... unless we are dealing with imaginary
numbers, which is well out of scope for this subject!)countTo as its argument
to then(), which means that countTo() will be called if the squareroot promise succeeds. In a similar way to squareroot(), countTo() will receive the previous resolve value, i.e. the square root, as a parameter.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
code using async/await:
async function doMaths(a, b) {
try {
const divisionResult = await divide(a, b);
const squareRoot = await squareroot(divisionResult);
await countTo(squareRoot);
console.log("All promises fulfilled!");
} catch(e) {
console.error(e);
}
}
Note how we're doing the whole procedure using sequential
code, even though the code is promise-based and asynchronous.
The key things are:
doMaths() is declared as async, which makes it a special kind of function (an AsyncFunction)
allowing us to await the result of promises without completely pausing the application;fetch()
s preceded by await. What await() does is suspend execution of the rest of the async function until the
promise concerned has been fulfilled. The argument to the resolve function of the promise is then returned from the await call.
await calls must be placed in an async function.async/await, it allows us to write code to await the successful resolution of the three promises (divide, square root and count to) sequentially, as if it was regular synchronous code. This is equivalent to promise chaining using then.await will only work with a promise. It's
awaiting the successful resolution of that promise. Trying to use
await without a promise will not work!
Note how we handle errors with a try/catch construct. We try to do the promise based, asynchronous code and
catch any promise rejections in the catch block. The catch block gets passed the error message accompanying the promise rejection.