When you start learning JavaScript, it won’t be long before you hear the term “callback function”. Callbacks are an integral part of the JavaScript execution model, and it’s important to have a good understanding of what they are and how they work.
- What Are JavaScript Callbacks?
- Why Do We Need Callback Functions?
- How to Create a Callback Function
- Different Kinds of Callback Functions
- Common Use Cases for JavaScript Callback Functions
- Synchronous vs Asynchronous Callbacks
- Things to Be Aware of When Using Callbacks
What Are JavaScript Callbacks?
Table of Contents
In JavaScript, a callback is a function that’s passed as an argument to a second function. The function which receives the callback decides if and when to execute the callback:
function myFunction(callback) { callback()
} function myCallback() { } myFunction(myCallback);
In the example above, we have two functions: myFunction
and myCallback
. As the name implies, myCallback
is used as a callback function, and we pass it to myFunction
as an argument. myFunction
can then execute the callback when it’s ready to do so.
Lots of blog posts will say that callbacks are called callbacks because you’re telling some function to call you back when it’s ready with an answer. A less confusing name would be “callafter”: that is, call this function after you’re done with everything else.
Why Do We Need Callback Functions?
You’ll often hear people say that JavaScript is single-threaded. This means that it can only do one thing at a time. When performing a slow operation — such as fetching data from a remote API — this could be problematic. It wouldn’t be a great user experience if your program froze until the data was returned.
One of the ways that JavaScript avoids this bottleneck is by using callbacks. We can pass a second function as an argument to the function that’s responsible for the data fetching. The data fetching request is then started, but instead of waiting for a response, the JavaScript interpreter continues executing the rest of the program. When a response is received from the API, the callback function is executed and can do something with the result:
function fetchData(url, cb) { cb(res);
} function callback(res) { } fetchData('https://sitepoint.com', callback);
JavaScript is an event-driven language
You’ll also hear people say that JavaScript is an event-driven language. This means that it can listen for and respond to events, while continuing to execute further code and without blocking its single thread.
And how does it do this? You guessed it: callbacks.
Imagine if your program attached an event listener to a button and then sat there waiting for someone to click that button while refusing to do anything else. That wouldn’t be great!
Using callbacks, we can specify that a certain block of code should be run in response to a particular event:
function handleClick() { } document.querySelector('button').addEventListener('click', handleClick);
In the example above, the handleClick
function is a callback, which is executed in response to an action happening on a web page (a button click).
Using this approach, we can react to as many events as we like, while leaving the JavaScript interpreter free to get on with whatever else it needs to do.
First-class and higher-order functions
A couple more buzzwords that you might encounter when learning about callbacks are “first-class functions” and “higher-order functions”. These sound scary, but really they aren’t.
When we say that JavaScript supports first-class functions, this means that we can treat functions like a regular value. We can store them in a variable, we can return them from another function and, as we’ve seen already, we can pass them around as arguments.
As for higher-order functions, these are simply functions that either take a function as an argument, or return a function as a result. There are several native JavaScript functions that are also higher-order functions, such as setTimeout
. Let’s use that to demonstrate how to create and run a callback.
How to Create a Callback Function
The pattern is the same as above: create a callback function and pass it to the higher-order function as an argument:
function greet() { console.log('Hello, World!');
} setTimeout(greet, 1000);
The setTimeout
function executes the greet
function with a delay of one second and logs “Hello, World!” to the console.
Note: if you’re unfamiliar with setTimeout
, check out our popular setTimeout JavaScript Function: Guide with Examples.
We can also make it slightly more complicated and pass the greet
function a name of the person that needs greeting:
function greet(name) { console.log(`Hello, ${name}!`);
} setTimeout(() => greet('Jim'), 1000);
Notice that we’ve used an arrow function to wrap our original call to greet
. If we hadn’t done this, the function would have been executed immediately and not after a delay.
As you can see, there are various ways of creating callbacks in JavaScript, which brings us nicely on to our next section.
Different Kinds of Callback Functions
Thanks in part to JavaScript’s support for first-class functions, there are various ways of declaring functions in JavaScript and thus various ways of using them in callbacks.
Let’s look at these now and consider their advantages and disadvantages.
Anonymous Functions
So far, we’ve been naming our functions. This is normally considered good practice, but it’s by no means mandatory. Consider the following example that uses a callback function to validate some form input:
document.querySelector('form').addEventListener('submit', function(e) { e.preventDefault(); this.submit();
});
As you can see, the callback function is unnamed. A function definition without a name is known as an anonymous function. Anonymous functions serve well in short scripts where the function is only ever called in one place. And, as they’re declared inline, they also have access to their parent’s scope.
Arrow Functions
Arrow functions were introduced with ES6. Due to their concise syntax, and because they have an implicit return value, they’re often used to perform simple one-liners, such as in the following example, which filters duplicate values from an array:
const arr = [1, 2, 2, 3, 4, 5, 5];
const unique = arr.filter((el, i) => arr.indexOf(el) === i);
Be aware, however, that they don’t bind their own this
value, instead inheriting it from their parent scope. This means that, in the previous example, we wouldn’t be able to use an arrow function to submit the form:
document.querySelector('form').addEventListener('submit', (e) => { ... this.submit();
});
Arrow functions are one of my favorite additions to JavaScript in recent years, and they’re definitely something developers should be familiar with. If you’d like to find out more about arrow functions, check out our Arrow Functions in JavaScript: How to Use Fat & Concise Syntax tutorial.
Named Functions
There are two main ways to create named functions in JavaScript: function expressions and function declarations. Both can be used with callbacks.
Function declarations involve creating a function using the function
keyword and giving it a name:
function myCallback() {... }
setTimeout(myCallback, 1000);
Function expressions involve creating a function and assigning it to a variable:
const myCallback = function() { ... };
setTimeout(myCallback, 1000);
Or:
const myCallback = () => { ... };
setTimeout(myCallback, 1000);
We can also label anonymous functions declared with the function
keyword:
setTimeout(function myCallback() { ... }, 1000);
The advantage to naming or labeling callback functions in this way is that it aids with debugging. Let’s make our function throw an error:
setTimeout(function myCallback() { throw new Error('Boom!'); }, 1000);
Using a named function, we can see exactly where the error happened. However, look at what happens when we remove the name:
setTimeout(function() { throw new Error('Boom!'); }, 1000);
That’s not a big deal in this small and self-contained example, but as your codebase grows, this is something to be aware of. There’s even an ESLint rule to enforce this behavior.
Common Use Cases for JavaScript Callback Functions
The use cases for JavaScript callback functions are wide and varied. As we’ve seen, they’re useful when dealing with asynchronous code (such as an Ajax request) and when reacting to events (such as a form submission). Now let’s look at a couple more places we find callbacks.
Array Methods
Another place that you encounter callbacks is when working with array methods in JavaScript. This is something you’ll do more and more as you progress along your programming journey. For example, supposing you wanted to sum all of the numbers in an array, consider this naive implementation:
const arr = [1, 2, 3, 4, 5];
let tot = 0;
for(let i=0; i<arr.length; i++) { tot += arr[i];
}
console.log(tot);
And while this works, a more concise implementation might use Array.reduce
which, you guessed it, uses a callback to perform an operation on all of the elements in an array:
const arr = [1, 2, 3, 4, 5];
const tot = arr.reduce((acc, el) => acc + el);
console.log(tot);
Node.js
It should also be noted that Node.js and its entire ecosystem relies heavily on callback-based code. For example, here’s the Node version of the canonical Hello, World! example:
const http = require('http'); http.createServer((request, response) => { response.writeHead(200); response.end('Hello, World!');
}).listen(3000); console.log('Server running on http://localhost:3000');
Whether or not you’ve ever used Node, this code should now hopefully be easy to follow. Essentially, we’re requiring Node’s http
module and calling its createServer
method, to which we’re passing an anonymous arrow function. This function is called any time Node receives a request on port 3000, and it will respond with a 200 status and the text “Hello, World!”
Node also implements a pattern known as error-first callbacks. This means that the first argument of the callback is reserved for an error object and the second argument of the callback is reserved for any successful response data.
Here’s an example from Node’s documentation showing how to read a file:
const fs = require('fs');
fs.readFile('/etc/hosts', 'utf8', function (err, data) { if (err) { return console.log(err); } console.log(data);
});
We don’t want to go very deep into Node in this tutorial, but hopefully this kind of code should now be a little easier to read.
Synchronous vs Asynchronous Callbacks
Whether a callback is executed synchronously or asynchronously depends on the function which calls it. Let’s look at a couple of examples.
Synchronous Callback Functions
When code is synchronous, it runs from top to bottom, line by line. Operations occur one after another, with each operation waiting for the previous one to complete. We’ve already seen an example of a synchronous callback in the Array.reduce
function above.
To further illustrate the point, here’s a demo which uses both Array.map
and Array.reduce
to calculate the highest number in a list of comma-separated numbers:
See the Pen Back to Basics: What is a Callback Function in JavaScript? (1) by SitePoint (@SitePoint)
on CodePen.
The main action happens here:
const highest = input.value .replace(/s+/, '') .split(',') .map((el) => Number(el)) .reduce((acc,val) => (acc > val) ? acc : val);
Going from top to bottom, we do the following:
- grab the user’s input
- remove any whitespace
- split the input at the commas, thus creating an array of strings
- map over each element of the array using a callback to convert the string to a number
- use
reduce
to iterate over the array of numbers to determine the biggest
Why not have a play with the code on CodePen, and try altering the callback to produce a different result (such as finding the smallest number, or all odd numbers, and so on).
Asynchronous Callback Functions
In contrast to synchronous code, asynchronous JavaScript code won’t run from top to bottom, line by line. Instead, an asynchronous operation will register a callback function to be executed once it has completed. This means that the JavaScript interpreter doesn’t have to wait for the asynchronous operation to complete, but instead can carry on with other tasks while it’s running.
One of the primary examples of an asynchronous function is fetching data from a remote API. Let’s look at an example of that now and understand how it makes use of callbacks.
See the Pen Back to Basics: What is a Callback Function in JavaScript? (2) by SitePoint (@SitePoint)
on CodePen.
The main action happens here:
fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(json => { const names = json.map(user => user.name); names.forEach(name => { const li = document.createElement('li'); li.textContent = name; ul.appendChild(li); }); });
The code in the above example uses the FetchAPI to send a request for a list of dummy users to a fake JSON API. Once the server returns a response, we run our first callback function, which attempts to parse that response into JSON. After that, our second callback function is run, which constructs a list of usernames and appends them to a list. Note that, inside the second callback, we use a further two nested callbacks to do the work of retrieving the names and creating the list elements.
Once again, I would encourage you to have a play with the code. If you check out the API docs, there are plenty of other resources you can fetch and manipulate.
Things to Be Aware of When Using Callbacks
Callbacks have been around in JavaScript for a long time, and they might not always be the best fit for what you’re trying to do. Let’s look at a couple of things to be aware of.
Beware of JavaScript Callback Hell
We saw in the code above that it’s possible to nest callbacks. This is especially common when working with asynchronous functions which depend upon each other. For example, you might fetch a list of movies in one request, then use that list of movies to fetch a poster for each individual film.
And while that’s OK for one or two levels of nesting, you should be aware that this callback strategy doesn’t scale well. Before long, you’ll end up with messy and hard-to-understand code:
fetch('...') .then(response => response.json()) .then(json => { fetch('...') .then(response => response.json()) .then(json => { fetch('...') .then(response => response.json()) .then(json => { fetch('...') .then(response => response.json()) .then(json => { }); }); }); });
This is affectionately known as callback hell, and we have an article dedicated on how to avoid it here: Saved from Callback Hell.
Prefer more modern methods of flow control
While callbacks are an integral part of the way JavaScript works, more recent versions of the language have added improved methods of flow control.
For example, promises and async...await
provide a much cleaner syntax for dealing with the kind of code above. And while outside the scope of this article, you can read all about that in An Overview of JavaScript Promises and Flow Control in Modern JS: Callbacks to Promises to Async/Await.
Conclusion
In this article, we examined what exactly callbacks are. We looked at the basics of JavaScript’s execution model, how callbacks fit in to that model, and why they’re necessary. We also looked at how to create and use a callback, different kinds of callbacks, and when to use them. You should now have a firm grasp of working with callbacks in JavaScript and be able to employ these techniques in your own code.
We hope you enjoyed reading. If you have any comments or questions, feel free to hit James up on Twitter.