What are Callbacks in JavaScript?

James Hibbard
Share

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.

  1. What Are JavaScript Callbacks?
  2. Why Do We Need Callback Functions?
  3. How to Create a Callback Function
  4. Different Kinds of Callback Functions
  5. Common Use Cases for JavaScript Callback Functions
  6. Synchronous vs Asynchronous Callbacks
  7. Things to Be Aware of When Using Callbacks

What Are JavaScript Callbacks?

In JavaScript, a callback is a function that is passed as an argument to another function and is executed when the receiving function completes its task. In other words, when the receiving function is done with its task, it calls the callback function to continue the execution of the program.

Here’s an example to help illustrate the concept:

Say you want to download a file from a server using JavaScript. The server may take some time to process your request and return the file, so you don’t want your application to stop and wait for the server to respond. Instead, you want to allow the user to continue using the program while the server processes the request in the background.

This is where a callback function comes in. You can pass a callback function as an argument to the function that makes the server request. When the server responds, it calls the callback function to continue the execution of the program. This way, the user can continue using the program while the server processes the request.

function downloadFile(url, callback) {
    // make server request
    // wait for response
    // when response received, execute callback function 
    callback(); 
}

function displayFile() {
    console.log('File downloaded successfully.');
}

downloadFile('https://yourfileserver.com/image.png', displayFile);

In the above example, downloadFile() is the function that makes the server request, and displayFile() is the callback function that gets executed when the response is received. The downloadFile() function takes two arguments: the URL of the file to download and the callback function to execute when the response is received.

Callbacks are a fundamental concept in JavaScript, especially when it comes to asynchronous programming. They allow us to perform tasks in the background and execute certain code only when a specific task is completed.

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) {
  // 1. Make API request to url
  // 2. If response successful, execute callback
  cb(res);
}

function callback(res) {
  // Do something with results
}

// Do something
fetchData('https://sitepoint.com', callback);
// Do something else

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() {
  // Do something (e.g. validate a form)
  // in response to the user clicking a button
}

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();
  // Do some data validation
  // If everything looks ok, then...
  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);
// [1, 2, 3, 4, 5]

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) => {
  ...
  // Uncaught TypeError: this.submit is not a function
  // `this` points to the window object, not to the form
  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);

// Uncaught Error: Boom!
// myCallback  file:///home/jim/Desktop/index.js:18
// setTimeout handler*  file:///home/jim/Desktop/index.js:18

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);

// Uncaught Error: Boom!
// <anonymous>  file:///home/jim/Desktop/index.js:18
// setTimeout handler*  file:///home/jim/Desktop/index.js:18

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); //15

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);
// 15

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 => {
    // Do some processing
    fetch('...')
      .then(response => response.json())
      .then(json => {
        // Do some more processing
        fetch('...')
          .then(response => response.json())
          .then(json => {
            // Do even processing
            fetch('...')
              .then(response => response.json())
              .then(json => {
                // Do yet more processing
              });
          });
      });
  });

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 me up on Twitter.

FAQs About Callbacks in JavaScript

What is a callback in JavaScript?

A callback in JavaScript is a function passed as an argument to another function. It is then executed after the completion of some asynchronous operation or at a specified time.

How do callbacks help with asynchronous operations?

Callbacks are essential for handling asynchronous operations in JavaScript. By passing a callback function as an argument, you can specify what should happen once the asynchronous task is complete, ensuring the non-blocking nature of JavaScript.

Are all callbacks used for handling asynchronous tasks?

No, callbacks can also be used for synchronous tasks. They are versatile and can be employed in various scenarios, not just for handling asynchronous operations.

How can callbacks be used to handle errors in JavaScript?

Callbacks can be used to handle errors by passing an additional parameter to the callback function, typically reserved for an error object. If an error occurs during the execution of the function, it can be passed to the callback, allowing proper error handling.

Are there any alternatives to callbacks for handling asynchronous code in JavaScript?

Yes, there are alternatives like Promises and async/await. Promises provide a more structured way to handle asynchronous operations, while async/await simplifies the syntax further, making asynchronous code appear more like synchronous code.

Are there any potential issues with using callbacks?

Callback hell, also known as the “pyramid of doom,” can be a common issue when dealing with multiple nested callbacks. This can lead to code that is hard to read and maintain. To mitigate this, alternative approaches like Promises or async/await can be considered.