Asynchronous Airport Data Load

Learning Objectives

Pre-requisites

Lesson

Synchronous 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:

  1. Use a callback function
  2. Use Promises
  3. Use 'async await'

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?

Promises

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.

Async await

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.

Assignment

  1. In pairs, explain to each other the differences between synchronous and asynchronous functions, and how you can tell the difference in your code.

  2. Create a new Node.js project for this assignment.

  3. Use the three different ways of forming async functions to read file content in your Airport class

  4. Write async tests for each of the three async functions using Jest

  5. Commit all your code into Github and share the link with your coach for review.

Assignment extension tasks

Research how to use Jest mocks to mock the reading of a file and simulate error scenarios such as the file not being found.

Additional resources