- Why Do We Need Callback Functions?
- How to Create a Callback Function
- Different Kinds of Callback Functions
- Synchronous vs Asynchronous Callbacks
- Things to Be Aware of When Using Callbacks
function myFunction(callback) callback() function myCallback() myFunction(myCallback);
In the example above, we have two functions:
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?
function fetchData(url, cb) cb(res); function callback(res) fetchData('https://sitepoint.com', callback);
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).
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.
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);
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
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.
Different Kinds of Callback Functions
Let’s look at these now and consider their advantages and disadvantages.
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 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(); );
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);
const myCallback = () => ... ; setTimeout(myCallback, 1000);
We can also label anonymous functions declared with the
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.
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);
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.reduce to calculate the highest number in a list of comma-separated numbers:
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
reduceto 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
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.
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
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
For example, promises and
We hope you enjoyed reading. If you have any comments or questions, feel free to hit James up on Twitter.