The Ultimate Guide to Callback Functions in JavaScript

Callback functions are one of those concepts every JavaScript developer should understand. Getting an understanding of what they are and how to use them is often a common problem for beginner programmers learning asynchronous concepts of JavaScript. In this article, we are going to turn this complex concept simple.

Understanding What Callbacks Are

Callbacks are functions in JavaScript that are passed as arguments to another function and are triggered or executed inside that other function. Callback functions are generally triggered after all statements in another function are executed. Let’s take a look a look at the following illustration to visualize this concept.

How callback functions work

In the previous image, we have two functions: function A() and function B(), in which function A() is triggered inside function B() when we pass function A() as an argument of function B().

The concept of callbacks can become confusing among beginner JavaScript programmers as it sounds confusing the idea of passing a function as a parameter. In JavaScript, you can pass any argument to a function regardless of whether it is a string, a number, an array, an undefined value, a null value, and also a function as you saw with the concept of callbacks.

How to Use Callback Functions

Now that you understand what callback functions are, it is time to put that knowledge and write some code. Initially, we are going to translate the example you saw in the previous image into code.

Let’s take a look at how a callback function would look in code.

// define function to be used as a callback function
function A() {
   console.log('function A');
}

// define function that triggers callback function
function B(callback) {
   callback();
}

// execute function that triggers callback function
B(A); // expected log: 'function A'

In this example, we are defining two functions A and B, which function B is the one executing function A. You can pass any callback function to function B.

function Z() {
   console.log('function Z');
}

function X() {
   console.log('function X');
}

function Y() {
   console.log('function Y');
}

B(Z); // expected log: 'function Z'
B(X); // expected log: 'function X'
B(Y); // expected log: 'function Y'

Notice how in this example, we defined functions Z, X, and Y and passed them as callback functions inside function B resulting in the execution of a different logic every time.

You can also pass and define a callback function at the same time.

B(
  function W() {
     console.log('function W');
  }
); // expected log: 'function W'

Anonymous Function Callbacks

It is also possible to pass an anonymous function as a callback. Remember, anonymous functions are functions without a specific name. Let’s take the previous example, and remove the W to automatically make the function anonymous.

B(
  function () {
     console.log('anonymous function');
  }
); // expected log: 'anonymous function'

Defining the function when passing it as an argument is possible, it is not recommended to do this practice as it provides two problems:

  1. Code is confusing and hard to understand
  2. Cannot call function W or the anonymous function outside of a function B

Callback Functions using ES6 Syntax

ES6 allows for generating arrow function expressions. Arrow function expressions allow defining a function without the need of using the keyword function while also using the expression => between the arguments and the body brackets. Let’s check how they look when used as callbacks.

// calbacks defined using arrow function expressions
B(
  () => {
     console.log('anonymous function');
  }
); // expected log: 'anonymous function'

B(
  () => console.log('one liner anonymous function')
); // expected log: 'one liner anonymous function'

const anotherOneLinerFn = () => console.log('another one liner anonymous function');
B(anotherOneLinerFn); // expected log: 'another one liner anonymous function'

Order of Callback Execution

All of our previous examples can make new programmers wonder whether there is a real need for function B to be defined in the first place as the only thing it does is execute callbacks.

Why not call the function directly instead of a callback?

Remember, we can add any additional logic to the function executing all the callbacks. Hence, if we decide to modify function B to execute other logic, we can do it and still execute the callback functions.

// modifying logic of the function that triggers callback function
function B(callback) {
   // function's B internal logic
   const date = new Date();
   console.log('This is the year ' + date.getFullYear());
   
   // execute callback function at the end
   callback();
}

In that way, regardless of what callback function is passed to function B, function B will always log This is the year ${currentYear}.

B(A); 
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function A'

B(Y); 
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function Y'

B(X); 
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function X'

Notice how callback functions are executed at the end of function B. However, you can modify the logic to best fit your solution. As mentioned in the beginning, it is common to execute callback functions at the end of a function, but they don’t have to be executed at the end of a function all the time. It is possible to trigger callback functions at the top, middle, or end of any other function.

// modifying logic of the function that triggers callback function
function B(callback) {
   // execute callback function right away prior executing additional logic
   callback();

   // function's B internal logic
   const date = new Date();
   console.log('This is the year ' + date.getFullYear()); 
   
}

By moving the position where the callback is triggered, the result will change whenever we trigger function B.

B(A); 
// expected result
// expected log: 'function A'
// expected log: 'This is the year 2021'

B(Y); 
// expected result
// expected log: 'function Y'
// expected log: 'This is the year 2021'

B(X); 
// expected result
// expected log: 'function X'
// expected log: 'This is the year 2021'

Callback functions will execute as long as you call them inside another function. Otherwise, callback functions will never run their logic. You can see this in the following function C definition.

// function accepting callback function, but doesn't execute callback function
function C(callback) {
   // function's C internal logic
   console.log('Function C is not calling any callbacks'); 
}

If we decide to trigger function C with different callback functions such as A, Z, X, or Y, we will always get the same output.

C(A); 
C(Z);
C(X);
C(Y);

// expected results
// expected log: 'Function C is not calling any callbacks

With this last example, you can visualize the power of callbacks. It allows running the same internal logic of a function while also triggering a callback function that adds additional functionality to an existing function without explicitly defining such functionality inside the function.

Types of Callback Functions

There are two types of callback functions: synchronous and asynchronous.

Synchronous Callbacks

Synchronous callbacks are triggered sequentially in another function following the order of operations defined insider another function. Unless the callback execution is completed, they prevent other functions from calling callbacks to complete all its internal logic. Let’s take a look at the following diagram.

Synchronous Callbacks

As you noticed, the getCurrentMonth function executes four different processes:

  1. Get Current Date
  2. Log Current Date
  3. Execute Callback
  4. Return Month

In code, this would look something along the following lines:

function getCurrentMonth(callback) {
   // Get Current Date
   const date = new Date();

   // Log Current Date
   console.log('This is the current Date' + date.toString());
   
   // execute callback function
   callback();

   return date.getMonth() + 1;
}

Be Aware of Performance or Blocking Callbacks

Synchronous callbacks are used most of the time. However, if the callback functions are not performant, they will also affect the performance of functions executing the callback function. Let’s take a look at the following code

function goodPerformanceFn() {
  console.log('good performance');
}

function badPerformanceFn() {
  for (var p = 0; p < 1000; p++) {
    console.log('bad performance');
  }
}

function getCurrentMonth(callback) {
   const t0 = performance.now();

   // Get Current Date
   const date = new Date();

   // Log Current Date
   console.log('This is the current Date' + date.toString());
   
   // execute callback function
   callback();

   // log how long it took to trigger all processes prior returning month
   const t1 = performance.now();
   console.log('Took: ' + (t1 - t0) + 'msecs');
   
   return date.getMonth() + 1;
}

getCurrentMonth(goodPerformanceFn);
getCurrentMonth(badPerformanceFn);

If we execute this piece of code in a terminal or in the console of the browser’s developer tools, you will see how long it takes each process triggered by getCurrentMonth function to complete.

Problems with Synchronous Callback Functions in JavaScript

When executing the callback function goodPerformanceFn, it took 0.3msecs for the getCurrentMonth to complete its execution.

On the other hand, it took 95.4msec for the getCurrentMonth to complete its execution when calling badPerformanceFn callback function. It took 95 times longer for getCurrentMonth to complete.

Processes that take a long time to complete their execution block the loop. This is antipattern and it should be avoided unless it is absolutely necessary for a callback function process to finish.

Asynchronous Callbacks

Asynchronous callbacks are used to prevent blocking code execution inside the function calling a callback.

Asynchronous callbacks

If you remember our previous example, function getCurrentMonth was calling badPerformanceFn which took a lot of time affecting the performance of getCurrentMonth.

Let’s create another function called reFactoredBadPerformanceFn which a refactored version of the badPerformanceFn function.

function reFactoredBadPerformanceFn() {
  setTimeout(function () {
    for (var p = 0; p < 1000; p++) {
      console.log('refactored bad performance');
    }
  }, 0)
}

getCurrentMonth(reFactoredBadPerformanceFn)

Now, we won’t have to wait to get the month returned when triggering the getCurrentMonth with the callback reFactoredBadPerformanceFn.

If you are not very experienced with setTimeout, you might be confused as the timeout parameter is set to 0, which logically means to wait 0 milliseconds to execute the internal function handler. In theory, that is correct, but there is a catch.

Executing setTimeout(fn, 0) doesn’t guarantee the function will execute right away. In order to understand what is going on, we need to understand what the event loop is.

The event loop is a mechanism used by JavaScript in charge of executing code and the reason behind JavaScript’s asynchronous programming. Let’s check the following diagram.

JavaScript Event Loop

What happens is JavaScript executes processes that are placed in the call stack. In the case of the getCurrentMonth function, there will be the following processes inside the call stack:

  • Get Current Date
  • Log Current Date
  • Execute Callback
  • Return Month

Once the call stack finishes executing all of these processes from the getCurrentMonth function, the stack will be empty and it will pick any functions lined up in the callback queue based on the order they arrive.

When the call stack encounters an asynchronous operation, it sends that operation to the browser API or web API. This API will start a different thread to run those asynchronous operations. This prevents blocking the normal execution of processes in the call stack.

Once those asynchronous operations are completed, they are sent to the callback queue and eventually back to the call stack as soon as all the functions in front of the asynchronous operation response are picked and executed in the call stack. The method setTimeout is considered an asynchronous operation. Therefore, the event loop will move the setTimeout from the call stack to the Web API’s own thread and prevent from blocking other operations in the call stack.

Moving the TimeHandler function once the minimum timeout has completed inside the Web API’s thread

Then the setTimeout will trigger the TimeHandler callback function defined as the first parameter setTimeout(timeHandler, timeout). If you remember the setTimeout we had defined in thereFactoredBadPerformanceFn function, we log several messages using a loop.

function reFactoredBadPerformanceFn() {
  setTimeout(function () {
    for (var p = 0; p < 1000; p++) {
      console.log('refactored bad performance');
    }
  }, 0)
}

Hence, each of those logs will execute in the call stack until they are all completed.

Executing Processes from the TimeHandler function

Another example of an asynchronous callback is using the fetch method to make a request to an API.

function fetchData() {
  return fetch('https://dog.ceo/api/breeds/image/random')
    .then((response) => response.json())
    .then((data) => {
      console.log('Got the data ', data );
      return data;
    });
}

getCurrentMonth(fetchData);

That’s why, we don’t need to wait to get the current month from getCurrentMonth function when triggering fetchData as a callback function.

Example of Performance Using Async Callback Function

Are Callback Functions Dead?

Callback functions are not dead. In fact, there is a high chance you have been using them without even realizing they were called callback functions. A good example of this is when you need to find a specific element in an array.

const numbers = [1,2,3,4,5]

const numberFound = numbers.find((number) => number === 1);

console.log('number found ', numberFound);

Probably you didn’t notice it was a callback as it was written using an arrow function expression. However, we could still write using the typical function keyword.

const numbers = [1,2,3,4,5];

const numberFound = numbers.find(
  function (number) { 
    return number === 1
  }
);

console.log('number found ', numberFound);

You could assign a variable to the callback function as we do in this case for findNumber.

const numbers = [1,2,3,4,5]

const findNumber = function (number) { 
    return number === 1
 };

const numberFound = numbers.find(findNumber);

console.log('number found ', numberFound);

Popular Callback Functions used

Some of the most popular callback functions that are used come from using array methods.

  • string.replace(callback)
  • array.forEach(callback)
  • array.find(callback)
  • array.some(callback)
  • array.filter(callback)
  • array.reduce(callback)

Why Callback Functions Are Important

Callbacks are important because they allow you to define a process that should execute happen whenever a behavior gets triggered. If you are a front-end developer, an example of common behaviors you need to pass a callback function is when executing functions triggered by a DOM event. This could be:

  • onclick: The event occurs when the user clicks on an element
  • onfocus: The event occurs when an element gets focus
  • onblur: The event occurs when an element loses focus

However, there are more DOM events that can be wired up with callbacks.

A good way to think about this is whenever your boss tells you to call her back whenever you finish the development of a feature to assign you a new task. It would be pointless to call your boss when the development of the feature isn’t completed as she won’t give you another task until the current task is completed.

The same we can say about callbacks. We can tell the applications to open a modal whenever a user clicks on a “create account” button, or display error messages whenever an input loses focus and there is an invalid value for an email field.

Those familiar working with Node.js, which allows developing backend/API solutions with JavaScript, callbacks are used all the time. Due to the nature of JavaScript, processes are executed sequentially, one after another.

If we were to have a NodeJS API with two API endpoints and two people are making a request to two different endpoints at the same time, what happens behind the scenes is that one person will be the first executing an endpoint, and the other person will be next in the line to execute an endpoint as there is really no such thing as having two requests running at the same time for NodeJs.

Can you imagine how long will the second person have to wait in case the first person reaching an API endpoint takes a lot of time to get a response? Subsequently, the second person will have to wait for the first process to execute which was triggered by someone else, but also the process the second person intended to trigger in the first place.

In other words, one process is blocking the other.

That’s why, it is critical for NodeJS to not wait for processes like I/O to finish, but instead define callbacks that will trigger once these asynchronous processes finish using the event loop smart mechanism to prevent blocking the call stack.

Conclusion

All in all, callback functions are typically used to execute asynchronous processes when a task completes in JavaScript. Chances are you have used callbacks before without knowing about it if you use methods such as array.forEach or myString.toReplace.

However, be careful to avoid using callbacks without understanding the implications they could have in the performance of a program, especially when executing callbacks that block the event loop.

Was this article helpful?

Share it with other developers who want an in-depth and simple explanation about callbacks. You can also share your thoughts by replying on Twitter of Become A Better Programmer or to my personal account.