Today we are going to look at creating asynchronous functions in JavaScript.
JavaScript is a single threaded language. This means it has one call stack (where code is executed) and one memory heap (where objects are stored). JavaScript executes code in order and must finish executing a piece of code before moving onto the next. We call this synchronous, or blocking, execution. Other languages such as C++ and Java are multi-threaded and can execute multiple pieces of code at the same time. We refer to this as asynchronous or non-blocking execution.
The call stack is a data structure in the runtime of javascript that functions to organise the running or 'execution' of your code.
Last on first off. Imagine a stick over which you can place hoops. To get to the bottom hoop, all the other hoops have to come off first.
.
When your code is executed, javascript will run it in 2 passes. The first pass optimises your code for execution. The second pass actually runs your code, and it is in this second pass that javascript uses the stack.
Lets take the following code example:
function multiply(a, b) {
return a * b
}
function square(n) {
return multiply(n, n)
}
function printSquare(n) {
const result = square(n)
console.log(result)
}
printSquare(4)
Read the code above. First of all there are 3 function definitions, then one of those functions is called. When printSquare
is called it is put onto the stack. printSquare
is evaluated and calls square
which is added to the stack, square
calls multiply
which is added to the stack. multiply
does not call any other function so it returns the value 16. the return
keyword means that function pops off the stack, now inside square
that function is evaluated to 16, and returns so square
is then popped off the stack. Now back in printSquare
the called to square
is evaluated and assigned in memory to the variable result
. Next line console.log is called with 16 and the function implicitly returns (without a value) as there is nothing more to execute. See below:
Loupe is a little visualisation to help you understand how JavaScript's call stack/event loop/callback queue interact with each other) is a tool which helps you visualise how JavaScripts Call Stack, Event Loop and Callback Queue interact with each other.
Open up this link illustrating how synchronous code is executed
Can you see how each line is executed one at a time? Experiment by adding in the functions code above (make sure you add a line of code which calls the printSquare
function to kick it off!).
The stack is very help to know about. When your code errors, you often get a 'stack trace' as part of the error message. Being able to read the 'stack trace' can help us follow the executing code, and that can lead us to our piece of code that is causing the error.
Try running the code below in your browser.
function multiply(a, b) {
throw new Error(`can't multiply ${a} and ${b}`)
}
function square(n) {
return multiply(n, n)
}
function printSquare(n) {
const result = square(n)
console.log(result)
}
printSquare(4)
This is the error. Read the stack trace from the bottom up. What do you think the numbers like (<anonymous:5:12>) refer to?
function hello() {
hello()
}
hello()
This will break. Can you explain what is going on here? What other code might cause a max call stack size exceeded (stack overflow)?
Now you are familiar with the Call Stack, imagine what the impact will be if a function takes a long time to execute or has to wait on something, this function will stop all the other code executing. So how can we avoid this?
In the Browser (for front-end JavaScript) and Node (for back-end JavaScript), JavaScript run inside a runtime 'container'. The runtime includes additional components which are not part of JavaScript. These include:
Web APIs
(e.g. the DOM, Timers, Fetch API)Callback Queue
- which holds callback functions from events which have just completedEvent Loop
- which monitors the Callback Queue and the Call Stack and places callbacks from the Callback Queue onto the Call Stack when it is empty. There is an event loop for every browser tab.The Event Loop
is what allows asynchronous (non-blocking) operations to occur — despite the fact that JavaScript is single-threaded.
Asynchronous (async) functions such as setting times, reading files etc. are recognised by Node.js and are executed in a separate area from the Call Stack. Node polls (regularly checks) the computer for the completion of the async operation and, once the operation is complete, the callback is placed into the Callback Queue
. The Event Loop
waits for the Call Stack to be empty and then moves the pending callback onto the Call Stack. It wait as otherwise it would randomly interrupt the execution of whatever sequence of function calls were queued up on the stack.
Below is an example.
This reference video provides an excellent explanation of the interactions between the Call Stack, Event Loop and Callback Queue.
TODO - add full picture.
JavaScript does not have a built in timer feature. It uses the Timer API provided by the Node.js runtime to perform time-related operations. For this reason, timer operations are asynchronous.
setTimeout(callback, millis)
can be used to schedule code execution after a designated amount of milliseconds. It accepts a callback function as its first argument and the millisecond delay as the second argument.
When setTimeout
is called, the timer is started in Web APIs
part of the Node/Browser runtime. This frees the Call Stack up to continue executing code and only when the timer is complete and the Call Stack empty, does the callback get pushed to the Call Stack for execution.
Click on this link which uses Loupe to illustrates async timers.
Does the timer callback get executed at exactly 5 second after the timer is started? If not, why not?
Make an animation or slide show that illustrates the event loop for the following piece of code. app.post
is a route handler from an 'express' server it takes a string that is a path, and a route handler function. The route handler function is called when the server receives a POST request to the /users
route. Start your stack with a call to the createUser
function.
The route handlers in express are all on a timer, so if you don't call response.render
or response.send
within a time limit express will return a timeout error. Don't worry about this for now. Note that functions without a name are referred to as 'anonymous' functions.
Be ready to present your slides or animation back to the group.
app.post('/users', function createUser(request, response) {
User.findOrCreate({ where: request.body })
.then(function (user) {
user.getContacts()
.then(contacts => {
request.session.userId = user.id
response.render('profile', {user, contacts})
})
})
logging(`/users route called with ${request.body}`)
})
So far our code executes synchronously. That 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, they 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 session we are going to look at each of them. They are:
Above is a link to 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 our Airport instances.
To start with lets 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')
})
})
Here is the way we are going to start doing this. In our test you can see I'm using a callback function. In Node.js callbacks follow this signature and err
followed by your async value being returned. If there are no errors the err
object is null
. Run your tests.
Lets 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 our file content nicely parsed into JSON.
But what? in our test we should see it logged out. But we don't. Why do we not see the contents of the file?
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 your logging. 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 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.
So to use our code now it will look different in 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. 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 can 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 simple, 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.
That 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 them will enable you to start writing more complex code more quickly.