DEV Community

Cover image for Are Callbacks Always Asynchronous?
Marek Zaluski
Marek Zaluski

Posted on

Are Callbacks Always Asynchronous?

When I was learning JavaScript and first encountered the concept of callback functions, I was still trying to wrap my head around the whole "asynchronous" idea.

Because callback functions seemed to get involved every time something asynchronous was mentioned, I had the understanding that if you were talking about callback functions, then it meant that you were doing something asynchronous.

In fact, I've seen a lot of tutorials and courses that tell you that a callback function is a type of asynchronous function.

Well, this is actually not true.

It turns out that callbacks aren't necessarily asynchronous at all.

But in order for that to make sense, it's helpful to have a clearer definition of what a callback function actually is, and also a clearer definition of what "asynchronous" means.

What's the definition of a callback function?

You're in the presence of a callback when you can see this situation in your code:

  • A function is being called. (Let's name it function Alpha).
  • Some other function is being passed as an argument into that call. (Let's name it function Bravo).
  • There's an expectation that Alpha is taking on the responsibility of calling Bravo, at some point in time.

The shape of the code looks like this (which is the syntax of a function call with one argument):

alpha(bravo);
Enter fullscreen mode Exit fullscreen mode

If the situation matches the those three conditions, then Bravo is a callback function. It's being passed into a function call as an argument with the expectation that it will get called. That's a callback.

Let's take a look at an event handler example and we'll confirm that the above points are there.

If you're waiting for a mouse click on your page, you might use the addEventListener method. If you're using a framework like React, then the onClick prop works in almost the same way.

function doSomethingAfterClick() {
  numberOfClicks++; // Count the number of mouse clicks, just for the sake of example.
}

const button = document.getElementById('action-button');

button.addEventListener("click", doSomethingAfterClick); // here is the callback situation
Enter fullscreen mode Exit fullscreen mode

In this example, do we have...

  • ...a function being called?
    • Yes, we're calling addEventListener, our Alpha function.
  • ...a function being passed as an argument into the call?
    • Yes, we're passing doSomethingAfterClick, our Bravo function.
  • ...the expectation that Alpha will call Bravo at some point?
    • Yes, when a mouse click happens, we expect that doSomethingAfterClick will be called.

So we can conclude that yes, it's a callback function.

To clarify the 3rd point of the definition, it's helpful to ask: who is calling the callback function?

In our example, who is calling doSomethingAfterClick? It's not our own code, because we don't see doSomethingAfterClick being called. If we were calling it, then we would see the function call syntax which includes the brackets after the function name: doSomethingAfterClick(). We don't have that here.

So, we can conclude that addEventListener is the one who will be responsible for calling doSomethingAfterClick. Because even if we don't see the explicit function call, we know that addEventListener can take our callback and can do whatever it wants with it, which includes making sure that it gets called when that click event happens.

Callbacks can be synchronous or asynchronous

After talking about the definition of a callback function, we can see that callbacks don't have anything to do with the async concept at all. They're just regular functions, and they don't know or care whether they're going to be called asynchronously or not.

But what's the difference between a synchronous callback and an asynchronous callback? What does it even mean when we say that a callback is asynchronous?

How to tell if it's an asynchronous callback?

Instead of going deep under the hood to find a technical definition of what asynchronous code means, I think it would be more helpful to stay close to the surface and ask: what can we actually observe that's different between sync and async callbacks?

To figure it out, we need to know what happens and in what order. Let's write a timeline based on the above scenario with functions Alpha and Bravo.

  1. We call function Alpha and pass Bravo, our callback, as an argument (example: we call addEventListener)
  2. Alpha returns. (this happens right away).
  3. Bravo, our callback, gets called. (example: a mouse click event happens)

The important thing to notice is the order of #2 and #3. First, Alpha returns. Then, Bravo is called at some later time.

This tells us that it's an asynchronous function. The reason is that the only way for Alpha to return before Bravo is if it puts Bravo into the asynchronous queue, causing it to be called at a later point in time.

I like to use the term parent function to refer to Alpha. The parent function receives the callback and takes responsibility for calling the callback.

Here's how the relationship between the callback and the parent function looks on the timeline:

Alt Text

On the other hand, if we had a synchronous situation, then it would mean that Alpha is calling Bravo directly and therefore needs to wait until Bravo returns before it can return too.

How to tell that it's a synchronous callback?

How would that timeline look if we had a synchronous situation?

  1. We call function Alpha and pass Bravo as an argument
  2. Bravo, our callback, gets called.
  3. Alpha returns.

So the relationship between the parent function Alpha and the callback function Bravo now looks like this:

Alt Text

Here are some great examples of synchronous callbacks:

  • the forEach array method. forEach takes a callback and it calls that callback once for every item in the array, waiting for each call to return before forEach itself returns, meaning it's synchronous.
  • the map array method. It also takes a callback and calls it for every item in the array. And because it has to wait for each call's result before it can produce the final output array, it doesn't even have a choice but to be synchronous.
  • filter and reduce also work in the same synchronous way.

If we were to draw these examples, we'd actually draw the blue box being called multiple times, and those calls would all happen before the parent function returns.

You can also look at the code of a function to figure out if it uses sync or async callbacks. If you can see the callback being called directly, with the function call syntax (like callback()) then you know it's synchronous.

Here's a function that uses a callback synchronously, and we can know this with certainty because we can see that the callback gets called directly:

// Example of a sync callback
function callWithRandom(input, callback) {
  const output = Math.random() * input;
  callback(output); // the callback is being called directly, right here
}
Enter fullscreen mode Exit fullscreen mode

This matches what we see on the diagram, because the call to callback(output) must be completed before the JavaScript engine can reach the end of the parent function and return from it.

Conclusion

What's the relevant part of this story? Let's recap the main points.

  • A callback function is a function that gets passed as an argument into a parent function call.
  • There's an expectation that the callback can be called by the parent function.
  • A callback can be used synchronously or asynchronously.
  • There's a difference in the order in which things happen in the timeline, depending on whether the callback is used synchronously or asynchronously.

My next articles will cover more topics on the fundamentals of JavaScript execution, so click Follow to get notified about them.

Top comments (1)

Collapse
 
ihsansfd profile image
ihsansfd

Thanks this article has cleared up my misunderstanding of callback functions.