Callback Functions
Open the project folder
In our starter files, open the index.html page from this lessons folder:
10.Async-JavaScript > 02.Callback-functions
Introduction
We are now going to cover callback functions. A callback is the name given to a function that is called after something has happened. Normally they are used as a next step to take once something has successfully completed.
From what we covered previously, we know that async code can take time to complete. For example, this could be getting data or saving to a database.
Example
Let’s see how this can affect our code. This is the code provided for this lesson:
<body>
<ul></ul>
<script>
let users = [];
function getUsersFromDatabase() {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
}
getUsersFromDatabase();
</script>
</body>
The code starts with an empty unordered list. Then an empty users array. A typical app which has users would probably have a function to make a call to the database, and update this users array with the result. We have a function to simulate this and add two users.
Listing users
Create a listUsers function:
function listUsers() {}
This will take our users and display them in the browser:
function listUsers() {
users.forEach(function (user) {
const element = document.createElement('li');
const name = document.createTextNode(user.name);
element.appendChild(name);
document.querySelector('ul').appendChild(element);
});
}
listUsers();
This function creates a new li element for each array value. We should now see the two users in the browser.
Simulating a time delay
Our users array is already here in this same file. But realistically, we would get it from a database, this would be asynchronous and may take some time. If all is well the time delay would be small, but still a delay.
To simulate this delay, we can wrap a setTimeout around getting the users:
function getUsersFromDatabase() {
setTimeout(function () {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
}, 1000);
}
And call listUsers from inside:
function getUsersFromDatabase() {
setTimeout(function () {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
}, 2000);
// move listUsers function call to inside getUsersFromDatabase function
listUsers();
}
Leaving the browser now empty. No users have been listed since we are immediately looping over the users here:
function listUsers() {
users.forEach(function (user) {
const element = document.createElement('li');
const name = document.createTextNode(user.name);
element.appendChild(name);
document.querySelector('ul').appendChild(element);
});
}
But, because of the time delay, the users array is still empty.
This is a common issue to deal with when working with data stored externally. We ask for the data we need, but we need to be sure the data is available before we can safely access it.
There needs to be an order, so things don’t break. Even though we are calling the functions in the correct order, the time delay is causing the issue. We can see this with some console logs:
function getUsersFromDatabase() {
setTimeout(function () {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
// log from getUsersFromDatabase
console.log('getUsersfromDatabase function');
}, 2000);
listUsers();
}
getUsersFromDatabase();
function listUsers() {
// log from listUsers
console.log('list users');
users.forEach(function (user) {
const element = document.createElement('li');
const name = document.createTextNode(user.name);
element.appendChild(name);
document.querySelector('ul').appendChild(element);
});
}
The log inside of listUsers will run first. We know this is the wrong way around for what we need. It would be nice if the listUsers function was only called once the code in the setTimeout was deemed a success.
The callback function
And we can do this by introducing a callback function.
First, comment out the listUsers function call:
// listUsers();
Then pass this in as the callback function when we call getUsersFromDatabase:
getUsersFromDatabase(listUsers);
This will then be taken in as a parameter to the function:
// Like with any other parameter, this name is up to us, it does not have to be callback.
function getUsersFromDatabase(callback) {
// ...
And we can now call this function at the end of the setTimeout:
function getUsersFromDatabase(callback) {
setTimeout(function () {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
console.log('getUsersfromDatabase function');
// run callback function in place of calling listUsers
// listUsers();
callback()
}, 2000);
}
This will cause the two names to appear in the browser after the time delay. This solution is a synchronous callback. Remember that synchronous refers to code which runs in order, and one operation needs to finish before moving onto the next.
Using this example, once the users have successfully been retrieved from the database, simulated by the setTimeout, we can use, or callback the listUsers function. And this way things are kept in order.
Using additional function arguments
If we have additional arguments to pass to the function, this is fine too. Imagine we wanted to only get admin users from the database:
// 2. Role then can be taken into the function:
function getUsersFromDatabase(role, callback) {
setTimeout(function () {
users = [
{
name: 'Chris',
occupation: 'Developer',
},
{
name: 'Homer',
occupation: 'Safety Inspector',
},
];
console.log('getUsersfromDatabase function');
callback();
}, 2000);
}
// 1. Pass in the role we want to get:
getUsersFromDatabase('admin', listUsers);
We do this as usual by passing in the role, and just make sure the callback is last and everything still works. This method of passing in functions as function parameters is nothing new.
Comparing to previous examples
Looking back at a previous example:
button.addEventListener('click', function () {
alert('show me something!');
heading.innerText = 'HEEEEEEYYYYYY!!!!!!';
});
This example has a function as the last parameter. And this will once we click on an element. Also consider array methods we have looked at, such as forEach. For each element in an array, we run a function, and this is also a callback. So, this pattern is nothing new.
Callbacks are an older, more traditional way of doing this. We will look at more modern ways next, but callbacks are important to be aware of since they are still plenty of them around and in use today.
Callbacks are often subject to some negativity in the JavaScript world, and not because they don’t work, it is more because of how messy they can get.
Nested callbacks
This example is not too bad, we have one function which call's another when necessary. But the problem lies when we have a callback which calls back another function, which calls back another function and so on. The listUsers function may also need a callback, and that function may also need a callback too.
We can compare this to our lives where we can have tasks to complete in order. Say we wanted to drive the car. There is a sequence of events which must happen in a particular order:
- Before we drive, we must walk to the car.
- Before we walk to the car, we must find the keys.
- Before that, we must get dressed.
- Before we get dressed, we must wake up.
And this series of tasks each rely on the previous one to be completed first. This is comparable to the callback functions being called once the previous code is successful, and it is these multiple callbacks which can result in a mess.
To see an example of this, we can create some small demo functions:
function getOutOfBed() {
console.log('out of bed');
}
function findKeys() {
console.log('found keys');
}
function walkToCar() {
console.log('walking');
}
function driveCar() {
console.log('yey!!!');
}
And using what we already know, each one needs to be passed the next function as a callback, then call this inside the functions:
function getOutOfBed(callback) {
console.log('out of bed');
callback();
}
function findKeys(callback) {
console.log('found keys');
callback();
}
function walkToCar(callback) {
console.log('walking');
callback();
}
// not needed for last function since it is not calling back anything else
function driveCar() {
console.log('yey!!!');
}
The callbacks are passed in, but we will need to set off this chain of functions. Create a function to begin this:
// …
function driveCar() {
console.log('yey!!!');
}
// 1. create function
function completeTask() {}
// 2. call this function, passing in first function to call back
completeTask(getOutOfBed);
What we do now may look complex, but this set up is just like the getUsersFromDatabase function from before, only this time there is more than one function in the chain to call back.
Inside of the completeTask function, we then nest each one of our functions. Add the first function to call was getOutOfBed:
function completeTask() {
getOutOfBed(function () {});
}
The console will show the following:
out of bed
The first function is called. The next part is key to understand, this getOutOfBed function takes in a callback:
function getOutOfBed(callback) {
console.log('out of bed');
callback();
}
Meaning when we call getOutOfBed below, we need to pass in this function to call back:
function completeTask() {
getOutOfBed(function () {
});
}
Pass in the next function in the chain, which is findKeys:
function completeTask() {
getOutOfBed(function () {
findKeys();
});
}
And this also needs to take in a function too:
function completeTask() {
getOutOfBed(function () {
findKeys(function () {
});
});
}
The next stage is walkToCar:
function completeTask() {
getOutOfBed(function () {
findKeys(function () {
walkToCar(function () {});
});
});
}
And finally, we can drive the car:
function completeTask() {
getOutOfBed(function () {
findKeys(function () {
walkToCar(function () {
driveCar();
});
});
});
}
The driveCar function is last and takes in no callback.
Leaving the following order in the console:
out of bed
found keys
walking
yey!!!
If you want to, additional code can also be passed into each function:
function completeTask() {
console.log('step 1');
getOutOfBed(function () {
console.log('step 2');
findKeys(function () {
console.log('step 3');
walkToCar(function () {
console.log('step 4');
driveCar();
});
});
});
}
The main idea here is each function will run the code inside, and then call back another function when completed. One step finishes, and moves onto the next one.
If this is starting to look complex, that’s understandable since it is getting a bit messy. And that’s why some alternative ways of dealing with this have been created. But just because there are newer ways, it doesn’t mean this is not relevant. Callbacks are an important part of JavaScript.
Coming up, we will discover some alternative ways of handling this, including using callbacks asynchronously with promises.