Object.keys
as a way to access the keys of an objectSynchronous code means the code in the line above has been evaluated and any values are available for us to use on our current line. Async functions do not return straight away. For example, if we want to read something from disc, that is an async function. It will not return immediately.
There are 3 ways to write and get values from async functions and in this lesson we are going to look at each of them. They are:
As part of the Pre-requisites for this lesson you should have downloaded a file with 28,000 airports in it. The file is in JSON so we can read it into our JavaScript programme and use that data to augment an Airport instance.
To start with, let's write a test like this.
test('airports have a city', () => {
const CDG = new Airport('CDG')
CDG.getInfo((err, info) => {
console.log(info)
expect(err).toBeNull()
expect(info.country).toEqual('FR')
})
})
In our test you can see we're using a callback function. In Node.js callbacks follow this signature with an err
followed by your async value being returned. If there are no errors the err
object is null
. Try running the test.
Let's turn to our Airport
class and write the getInfo
function (that will take a callback). You will have to require the fs
or 'file system' module from Node.js.
const fs = require('fs')
// add this function to your Airport class definition
getInfo(callback) {
fs.readFile('./airports.json', (err, data) => {
callback(err, JSON.parse(String(data)))
})
}
This is async code. We read the file from disk. The file contents comes out as a Buffer - you can console.log
it to have a look at it. We need to turn the Buffer into a String, then that String we turn into a JavaScript object using JSON.parse
. Finally, we call the callback with an error if there is one or if not, we return our file content nicely parsed into JSON.
In our test we expect to see the contents of airport file logged out but we don't! Why not?
The reason is that the test is called synchronously, it does not wait for the result of calling CDG.getInfo
. To test an async function in Jest, pass in a done
function and then call it when you are done.
test('airports have a city', (done) => {
const CDG = new Airport('CDG')
CDG.getInfo((err, info) => {
console.log(info)
expect(err).toBeNull()
expect(info.country).toEqual('FR')
done()
})
})
Can you see how that is working? You should now see the file contents being logged.
Look at one of the entries in the airport data. We want to filter out an airport using the 'iata' code. Can your getInfo
function to filter out the right airport and return just that data point?
Another way to write and organise async code is using Promises. Lets refactor our getInfo function to return a Promise.
getInfo() {
return new Promise((resolve, reject) => {
fs.readFile('./airports.json', (err, data) => {
if (err) return reject(err)
const airports = JSON.parse(String(data))
const [airport] = Object.keys(airports)
.filter(airportCode => airports[airportCode].iata === this.name)
.map(airportCode => airports[airportCode])
resolve(airport)
})
})
}
Can you see the new
keyword? What does that tell you about a Promise? What do you initialise a Promise with? Our callback style structure that we use with the fs
module is wrapped in a Promise. Now when resolve is finally called it will trigger the .then
part of a Promise object.
To use Promises, we need to modify our test:
test('airports have a city', () => {
const CDG = new Airport('CDG')
return CDG.getInfo()
.then(info => {
expect(info.city).toEqual('Paris')
})
.catch(err => {
expect(err).toBeNull()
})
})
Notice now we don't need the done
callback in the test, this is because we are returning a Promise from our test, and Jest will figure this is an async test and will wait for the Promise to resolve or reject.
The Promise object is 'thenable' - you can chain a series of Promises together using then
like this:
return doSomeThing()
.then(thing => {
return theNextPromise(thing)
})
.then(next => {
return anotherPromise(next)
})
.catch(err => {
console.error('this catch block will catch any reject(err) in the chain.')
})
Take note you must return a Promise from the then
block if you want to keep chaining. This avoids the pattern of deeply nesting callbacks which some people find hard to read.
Finally, we can use the async
and await
keywords to make our asynchronous code read more synchronously.
Lets update our test:
test('can get information like the city from an airport instance', async () => {
const CDG = new Airport('CDG')
const airport = await CDG.getInfo()
expect(airport.city).toEqual('Paris')
})
Notice how we use the 2 keywords within our test. First of all we need to declare an async
function. We use the async
keyword before our function definition. Then inside the async function we use the await
keyword to pause and wait for our async value to resolve. That means we don't need the done
callback, we don't need to use a Promise with then
, we can just write it nice and simply, line by line. Jest knows that this is an async test because we used the async
keyword before the function definition.
Can you refactor your getInfo
function to use async await?
It's a bit tricky because fs.readFile
takes a callback. It's not really designed to work with async await. However from Node.js 11.0 onwards you can require a version of the fs
functions that are wrapped in a Promise object. Add this to the top of your Airport class:
const { readFile } = require('fs/promises')
Now you can use the readFile
function with the await
keyword because this readFile
function is wrapped in a Promise.
This is a lot to get your head around! Async functions are a key characteristic of JavaScript. Objects, functions and async are the building blocks of the language. Spending time now learning how to work with async functions will enable you to start writing more complex code more quickly.
In pairs, explain to each other the differences between synchronous and asynchronous functions, and how you can tell the difference in your code.
Create a new Node.js project for this assignment.
Use the three different ways of forming async functions to read file content in your Airport class
Write async tests for each of the three async functions using Jest
Commit all your code into Github and share the link with your coach for review.
Research how to use Jest mocks to mock the reading of a file and simulate error scenarios such as the file not being found.