So far, these notes have included several examples of passing functions as parameters to other functions:
-
Starting an express server:
app.listen(port, () => console.log(`The shopping list is running on http://localhost:${port}/`));
-
Adding a handler for an Express.js route:
app.get('/new', (req, res) => { // Get the user input const description = req.query['description']; const quantity = parseInt(req.query['quantity']); // Add to the shopping list items.push({ description, quantity }); // Redirect back to the home page to show the full list res.redirect('/'); });
-
Using the Fetch API:
// Find all items in the shopping list function queryItems(callback) { fetch('/api/items') .then(response => response.json()) .then(result => callback(result.items, result.total)); }
Another common example that occurs in JavaScript programming, is using setTimeout
to run a function after a delay:
console.log('Shown immediately');
setTimeout(
() => console.log('Shown after two seconds (2000 milliseconds)'),
2000
);
What all these examples have in common is a separation, in time, of the execution of code. An action starts by calling app.listen
, app.get
, fetch
and setTimeout
. The supplied function is invoked sometime later when the result or request is ready.
This supplied function is known as a callback and is a common way of dealing with tasks that take a long time. Callbacks avoid the problem of code getting ‘stuck’ waiting for slow services.
To compare JavaScript’s use of callbacks with other approaches, consider the following example. It uses the Python programming language to perform two requests:
import requests
r = requests.get('http://aws.amazon.com/')
print("Amazon has status code:", r.status_code)
r = requests.get('http://cloud.google.com/')
print("Google has status code:", r.status_code)
On a slow connection, the first request might take a long time. The second request will not start until the first request has finished. This wait for serial execution is said to be blocking code. [1]
Here is equivalent Node.js code:
// Fetch is automatically available in web browsers
// However, node-fetch is needed to use fetch from Node.js
const fetch = require('node-fetch');
fetch('http://aws.amazon.com')
.then(response => console.log('Amazon has status code:', response.status));
fetch('http://cloud.google.com')
.then(response => console.log('Google has status code:', response.status));
Running the equivalent JavaScript code multiple times highlights the difference with Python. Here’s what happens if I run the Node.js code on my computer:
$ node nodejs_statuses.js
Google has status code: 200
Amazon has status code: 200
$ node nodejs_statuses.js
Google has status code: 200
Amazon has status code: 200
$ node nodejs_statuses.js
Amazon has status code: 200
Google has status code: 200
$
Note that on the first two runs, Google’s server responded before Amazon, but on the third run, Amazon’s server responded first. Node.js is performing both requests simultaneously and executing the callbacks as soon as a result is available.
Callback “Christmas trees”
Unfortunately, callbacks can make code difficult to understand. The following script displays a timed countdown (“Three”, “Two”, “One”, “Go!”):
console.log('Three');
setTimeout(
() => {
console.log('Two');
setTimeout(
() => {
console.log('One');
setTimeout(
() => {
console.log('Go!');
},
1000
);
},
1000
);
},
1000
);
This is not readable code! The deep nesting of callbacks is jokingly known as “callback hell” or sometimes “callback Christmas trees” (because its shape resembles a Christmas tree on its side):
Using async / await
The ECMAScript 2017 standards introduced async
and await
to JavaScript. These keywords vastly simplify code that involves callbacks.
Here is a timed countdown using async/await:
const delay = require('delay');
async function countdown() {
console.log('Three');
await delay(1000);
console.log('Two');
await delay(1000);
console.log('One');
await delay(1000);
console.log('Go!');
}
countdown();
In JavaScript, async/await is syntactic sugar to make it easier to write callbacks:
-
await delay(1000)
means something like: “turn the rest of the function into a callback and pass it todelay(1000).then(callback)
” -
async function countdown()
means something like: “this function uses await and expects a callback”
The await
keyword is only permitted inside async
functions.
[2]
Promises
Internally, async/await depends on promises. In computing, a promise or a future refers to a placeholder representing a “promise” to return a value later.
Promises have been part of JavaScript since the ECMAScript 2015 standards. In JavaScript, a promise is a standardized way of defining callbacks that can be used consistently in libraries and with async
and await
.
In JavaScript, promises solve the problem of inconsistencies when passing callbacks as parameters. For example, in setTimeout(callback, milliseconds)
, the callback is the first parameter. In app.listen(port, callback)
, the callback is the last parameter. Instead, a function that expects a callback must return a Promise
, which has a consistent .then(successCallback, failureCallback)
method. [3]
I apologize if this terminology is confusing. The simplest way to think about promises is that instead of writing delay(1000, callback)
, you write delay(1000).then(callback)
.
Tip
|
The following terminology describes the state of a promise:
|
The Promise
class in JavaScript has several static helper functions. It provides methods to create promises that are instantly fulfilled or rejected:
-
Promise.reject(error)
creates a promise that is immediately rejected witherror
-
Promise.resolve(value)
returns a resolved promise with the resultvalue
The Promise
class also has helper functions to resolve multiple promises at once:
-
Promise.race(iterable)
returns the result of the first promise to resolve.
For example,Promise.race([delay(1000), delay(2000), delay(3000)])
is fulfilled in one second becausedelay(1000)
finishes first. -
Promise.all(iterable)
waits for all the promises to resolve.
For example,Promise.all([delay(1000), delay(2000), delay(3000)])
is fulfilled in three seconds because all start simultaneously butdelay(3000)
finishes last.
Promises, async/await and fetch
Promises in JavaScript establish a standard convention for passing callbacks. This standardization makes it possible for JavaScript to translate code involving await
into callbacks that invoke .then(…)
.
Async/await supports any API that returns promises (or objects with .then
methods). fetch
is an obvious candidate. Async/await will simplify the presentation logic code responsible for retrieving data from a server.
For example, recall the queryItems
function in the single-page application:
// Find all items in the shopping list
function queryItems(callback) {
fetch('/api/items')
.then(response => response.json())
.then(result => callback(result.items, result.total));
}
The function is simpler with async/await:
// Find all items in the shopping list
async function queryItems(callback) {
let response = await fetch('/api/items')
let result = await response.json())
return result;
}
Tip
|
Code using async/await is usually easier to read than callbacks. Async/await code is sequential and does not have nested callbacks. Internally, JavaScript uses callbacks, so there are no problems with blocking code. |
I find async/await to be very useful as a developer. Most modern packages available for Node.js support promises. However, when I search for new libraries, I make sure that long-running functions have support for promises, rather than callbacks.
Async/await will appear in the next chapter when I discuss databases. Database queries can involve network communication and long-running queries. Database client libraries that support promises allow for simpler code.
Tip
|
The reason fetch requires two calls to await is because there are two things that you may wait on as a developer. The headers and status are available after the first await , while the complete response is available after the second await . There are alternatives to fetch that work with a single await , such as axios or Angular’s @angular/common/http library.
|