JavaScript Callback functions are one of the main features of the language that allow you to implement some asynchronous programming patterns, and is very common in the language, as it is an event-driven language.
A lot of guides, tutorials, and how-to videos out there attempt to explain how this stuff works. The issue I take with them is that they do not explain callbacks in their simplest possible form. That's why I'm writing this guide. I'm going to break down each step, explaining the process of the callback function in it's most basic form so that you gain a fundamental understanding of how to use them. This guide may be kind of lengthy, but that's because I'm not holding any punches or skipping any details.
Yes, this topic is confusing. No, you're not bad at programming or whatever if it doesn't make sense. There is no shortage of ignorant developers out there that have full-time, high paying salaries that don't know how callback functions actually work. Don't feel bad if you're having trouble with this concept.
With that out of the way, lets get into it.
First off, lets take a look at a normal function.
function greet(name) {
console.log("Hello, " + name + "!");
}
Here, we define a function with one parameter. When this function is called, it's going to take that parameter as an argument, and print it to the console. greet("User")
will return Hello, User!
. If you're this far in your programming journey, this will hopefully make sense to you.
In the function definition, we give it name
just as a placeholder, and when you CALL the function, the compiler takes the argument you give it, and fits it into that name
placeholder. So when the function runs, any instance of name
will be "filled" with the argument you provided. In this case, we used the string of "User".
Now lets talk about what you clicked onto this article for.
But before we get into the weeds, let's get the most concrete definition of a Callback Function.
What is a Callback Function?
In JavaScript, a callback function is a function that is passed as an argument to another function and is executed after some operation has been completed.
Thanks to ChatGPT for the simple and concise explanation.
Now, lets write a callback function solely based on this definition.
function doSomething(callback) {
// Perform a set of operations here...
// Pull data from an API,
// Validate data, etc
callback();
}
function handleComplete() {
console.log("Completed!")
}
doSomething(handleComplete);
Alright, now we have two function definitions here, followed by a single function call.
The first function takes one argument, and then calls that argument as a function at the end of the function itself. This is the key concept in understanding callbacks. Even though we tell doSomething
in its definition that we will be passing in an argument, that argument can be a function. However, don't make the mistake of writing it like this:
function doSomething(callback()) { ... }
The key difference here, is that writing it like this tells the compiler to run a function called callback()
, which does not exist. At least, not in this snippet of code. Instead, remember that this parameter is just a placeholder.
Let's take a step back and look at when we call the doSomething
function.
We know that doSomething
is going to take a single argument. We pass in handleComplete
, but without the parentheses. Why? Because we aren't doing a function call inside of a function call. Doing that would look like this:
doSomething(handleComplete())
What we are doing is telling the compiler that we want doSomething
to hold onto an argument called handleComplete
. Then the doSomething
function is going to run. Then, it's going to get to the callback()
function, where it "plugs in" your argument's name, and THIS is what calls that function. doSomething
does everything it needs to, then pulls that argument and runs that as a function.
Now, if you do not have a function defined in your code called handleComplete
, you will get an error that handleComplete
is not a function. It's the same thing as trying to call a variable that holds a string as a function, like this:
let name = "Dustin"
name()
> TypeError: name is not a function
Now, we're going to turn it up a notch by introducing a callback function that itself takes arguments. So, lets write out some code that illustrates this pattern.
const itemsArray = [1, 2, 3, 4, 5]
function doThing(items, callback) {
// Doing stuff
callback(items);
}
function justArray(items) {
// Now the operations are finished
console.log(items)
}
function addArray(arr) {
// Do some arbitrary stuff with an array
arr.push(6, 7, 8)
console.log(arr)
}
doThing(itemsArray, justArray)
doThing(itemsArray, addArray)
Let's talk about how this works. So we obviously have a couple of functions here, just to help illustrate the point that you can plug in functions as a callback, so long as the argument name is a valid function name.
The doThing
function takes in some data as the first argument, and then as the second parameter, a function. So, even though the second parameter is where the magic happens (the reason it's a callback function), we know it's going to be a function, but the parameter ITSELF doesn't necessarily indicate that, because it's not written as doThing(items, callback())
. Notice in the actual code, the second parameter has no parentheses. It's just a normal parameter. However, INSIDE the function, we see that this parameter is getting called AS A FUNCTION. This is where the compiler says "Remember that parameter we specified earlier? Now call that as a function." And at runtime, or when the function is called, it's going to take that parameter, and call it as a function, passing in the argument (which in this case is just an array of numbers).
So now, look at when the callback function is being called, near the bottom, you'll notice that we aren't calling that outside function with an argument. But when we defined it, we told it that it's going to take an argument. So what gives? Well, when we call doThing
and provide the two arguments, we're using that array as the first argument, but the second argument is the NAME of a function that's been defined, but we aren't calling it here. It's called when the callback function runs, and that argument that holds the function, justArray
or addArray
, matches the name of the parameter. Remember, that the compiler calls that parameter name with parentheses and THAT is what calls it.
Now, we're going to back up and look at the main doThing
function again. Let's say that the function we're passing in is addArray
, so we'd do so by writing doThing(itemsArray, addArray)
. At this point in time, addArray
is just an argument name. But when the callback function reaches the point in the code where it says to do callback()
WITH THE PARENTHESES, the compiler says "Oh, that parameter is going to be called as a function" and runs it. If you try to run this argument without it being an actual function, the compiler will error out and tell you that the argument you tried to use is not a function. So, in essence, this is what running the callback function looks like.
So that's how they work. Before I wrap up here, let's look at a practical example of how you'll use a callback function in the wild.
// Get the element on which to attach the event listener
const element = document.getElementById("my-element");
// Create the event listener function
function eventListener() {
// This code will run when the specified event occurs
alert("The event occurred!");
};
// Attach the event listener to the element
element.addEventListener("click", eventListener);
This is one of the most common and perhaps most basic things you'll use callback functions for. In fact, I used eventListeners for awhile before actually knowing how they worked, or that they were even considered callback functions. First, we grab an element from the DOM that we can add a click to, then we define what we want that click to actually do. In this case, it's going to make your browser open an alert box that says The event occurred!
.
The important part I want to highlight is the function call itself, where we tell the compiler to add an event listener to the element we grabbed earlier, tell it that it's going to call a function called eventListener
when that element is clicked.
There's some JavaScript stuff going on under the hood here, but the important take-away is that we tell .addEventListener
that "click"
is the first argument, and the second one is eventListener
, the function. After the element is clicked and it does its "under the hood" stuff, the eventListener
function is going to be called, which again, just alerts your browser.
Another common use of callbacks is using them as anonymous functions.
element.addEventListener("click", () => {
console.log("I just got clicked.")
}
This is pretty useful, specifically for eventListeners just because you can specify one or multiple functions to be run when you click a button and you don't have to define an outside function for it to run. The concept is the same however. The main function .addEventListener
does its thing, then runs your function as a callback after it's done.
That is about all I have to explain for understanding callback functions. It's a difficult topic to cover, but I feel like understanding this fundamental building block as not just a web developer, but any kind of programmer will benefit from understanding this pattern. Hopefully this article helped you understand callback functions.
Top comments (4)
Will people please, please stop talking about callback functions in terms that make them seem as if they are some special language feature. They're not. They are just plain, ordinary functions.
Maybe my phrasing here wasn't clear enough.
I attempted to frame the use of callbacks as a core feature of the language, which sure, maybe is not specific to JavaScript (see: C, Python, Ruby or PHP), but the term "special" is kind of subjective and the only instance of that word on this page is used by you.
Ultimately, it's irrelevant whether or not they are "special", the article just attempts to explain how a commonly misunderstood, yet very commonly used design pattern works.
Thanks for reading.
In no way did I mean that you were implying they were special to JS. The way a lot of people write about callback functions creates the impression that they are somehow distinct from ordinary functions - which could not be further from the truth. I truly believe that they are 'commonly misunderstood' for this very reason.
The best way to explain callbacks in their 'simplest' form is just to explain that functions are just values - and can be stored and passed around in just the same way as any other - allowing you to use them in different places. Once that is understood, the whole concept becomes obvious. Trying to explain it without this merely creates this confusion / incorrect distinction.
Also, just a heads up - you can add syntax highlighting to your code blocks by putting
js
after the opening three backticks 👍No.