JavaScript Callback Functions

By the end of this article i’m going to make sure you understand how to use callback functions in JavaScript.

Functions Review

Before we get into callback functions, let’s just review functions a little bit first, because you really need a good understanding of functions in order to understand callback functions. So i’m going to make a function called funny that takes a string and logs it to the console in surrounded with laughing emojis so everyone knows the text is funny.

function funny(input) {
  const output = `🤣 ${input} 🤣`
  console.log(output)
}

So if you run this function with the string “spiders are the only web devs that like bugs”, it outputs with laughing emojis, because it’s a hilrious joke.

funny("spiders are the only web devs that like bugs")
> 🤣 spiders are the only web devs that like bugs 🤣

input contains the value of the text that is passed to the function, output is that text surrounded by laughing emojis, and the function logs it to the console.

Different ways to create a function

I am creating a function decleration here, but I could also create the function using a function expression like this:

const funny = function(input) {
  const output = `💩 ${input} 💩`
  return output
}

Or using an arrow function expression like this:

// Arrow function expressions
const funny = (input) => {
  const output = `💩 ${input} 💩`
  return output
}

For the sake of this video, these are all exuivalent. No difference. In the end you have a variable named funny that references a function. So it can be executed using the round brackets.

funny("spiders are the only web devs that like bugs")

And since functions are first class values, I could even create a second variable that references the same function if I wanted to:

const funnyAgain = funny
funnyAgain("spiders are the only web devs that like bugs")

It’s pointless in this case, but something that’s very important in JavaScript. Functions are first class values, so they can be assigned to a variable and passed around as arguments just like any other value.

Anyway, i’m going to go back to the function decleration for the rest of this video because I like using the word function. This still gives me a variable called funny that references the function.

function funny(input) {
  const output = `🤣 ${input} 🤣`
  console.log(output)
}

Reusable Functions

Functions can accept parameters, funny accepts input, but functions can also return values. So Instead of console logging the output inside the funny function, this function can return the output and I can console.log it somewhere else.

function funny(input) {
  const output = `🤣 ${input} 🤣`
  return output
}

const message = funny("spiders are the only web devs that like bugs")
console.log(message)

And this can be nice because it makes the function more generic and reusable. Before this refactor, funny could only be used to log text to the console; now I can choose to log the output to the console or do something else like add the new string to an array or display it on a web page or pretty much anything. funny is now more reusable.

But all functions don’t need to be generic and reusable like this. I will choose to put this new chunk of code into a function called funnySpiderJoke to clean things up a bit:

function funny(input) {
  const output = `🤣 ${input} 🤣`
  return output
}

function funnySpiderJoke() {
  const message = poop("spiders are the only web devs that like bugs")
  console.log(message)
}

funnySpiderJoke()

This gives my chunk of code a name and it makes it a bit cleaner if I want to do something like execute that logic multiple times. Let’s say 5:

for (let i = 0; i < 5; i++) {
  funnySpiderJoke()
}

funny is a very generic function, the output will always change based on the input, and I can use the output however I want. funnySpiderJoke is a specific function, it will always do the same thing no matter what. Both are functions and both are good.

Now I could wrap this new chunk of code into a function as well:

function funny(input) {
  const output = `🤣 ${input} 🤣`
  return output
}

function funnySpiderJoke() {
  const message = poop("spiders are the only web devs that like bugs")
  console.log(message)
}

function funnySpiderJokeFiveTimes() {
  for (let i = 0; i < 5; i++) {
    funnySpiderJoke()
  }
}

funnySpiderJokeFiveTimes()
> 🤣 spiders are the only web devs that like bugs 🤣
> 🤣 spiders are the only web devs that like bugs 🤣
> 🤣 spiders are the only web devs that like bugs 🤣
> 🤣 spiders are the only web devs that like bugs 🤣
> 🤣 spiders are the only web devs that like bugs 🤣

Callback Functions

funnySpiderJokeFiveTimes is a very specific function that will always print out that joke to the console 5 times. But it might be nice to make this function a little bit more generic and reusable. For example, instead of always printing this joke 5 times, I might want to be able to change that number to print it once or twice or a thousand times. So instead of always doing this 5 times, let’s change this to be a paramter.

function funnySpiderJokeSomeTimes(times) {
  for (let i = 0; i < times; i++) {
    funnySpiderJoke()
  }
}

const t = 2
funnySpiderJokeSomeTimes(t)

Anytime I call the funnySpiderJokeSomeTimes function, I can specify how many times this function should print the joke.

I can go one step further and make this even more generic by passing in the funnySpiderJoke function as a paramter as well.

function doSomethingSomeTimes(times, callback) {
  for (let i = 0; i < times; i++) {
    callback()
  }
}

const t = 2
doSomethingSomeTimes(t, funnySpiderJoke)

Remember that functions can be passed to another function just like a number or any other value in JavaScript can be passed to a function. The times parameter is a number and it’s used instead of the hard coded number 5. The callback parameter is a function so we call it with () because that’s what we do with functions.

In this example, times is 2 and callback is funnySpiderJoke, but these could be any number or function. doSomethingSomeTimes will work with any number and any function.

A callback function is a function passed into another function as an argument, which is then invoked by that function. So any function that is passed into doSomethingSomeTimes is a callback function since it’s passed into doSomethingSomeTimes as an argument and doSomethingSomeTimes is now responsible for invoking it using the ().

This is kind of amazing, because it allows us to create these much more reusable functions by allowing the specific logic to be passed into a function as a function. It makes our code more flexible and reusable.

Notice that I am not calling the funnySpiderJoke function. I am just passing it in as a paramter.

// Not This
doSomethingSomeTimes(t, funnySpiderJoke())

// This
doSomethingSomeTimes(t, funnySpiderJoke)

I don’t want to invoke the funnySpiderJoke function. I want doSomethingSomeTimes to invoke it some number of times. If I invoke the function, the logic will be executed just once, the one time I call the function, and I will be passing the return value of the function to the next function. I don’t want that, I want to pass the actual function to doSomethingSomeTimes.

Anonymous Functions

Instead of creating a new variable t to hold the number 2, I would probably just pass the number 2 in directly like this:

function doSomethingSomeTimes(times, callback) {
  for (let i = 0; i < times; i++) {
    callback()
  }
}

doSomethingSomeTimes(2, funnySpiderJoke)

It’s also very likely that I’ll want to pass in a function as a paramter, but I don’t want to create a new variable to hold the function. I want to pass in the function directly. I can do this with an anonymous function.

function doSomethingSomeTimes(times, callback) {
  for (let i = 0; i < times; i++) {
    callback()
  }
}

doSomethingSomeTimes(2, function() {
  console.log("a new function")
})

This will just print out "a new function" 2 times to the console. But it demonstrates that I can create the function without creating a new variable for it. I just create it when I call the doSomethingSomeTimes function.

And in modern JavaScript, it has become more common to use an arrow function for all callback functions.

doSomethingSomeTimes(2, () => {
  console.log("a new function")
})

In this example, the outcome will be identical, but in sometimes the outcome will be different if you use an arrow function. I won’t get into that in this video, but check the video description for more videos on JavaScript.

Just to recap, I can pass in a function as a paramter to a second function and that second function is now responsible for calling the first function. The first function is now the callback function. This makes the code much more reusable and flexible.

Passing Values to Callbacks

Let’s take this one step further. We know that we can pass a value to a function, that was the first example in this post. But what if we want to pass a value to a function that is a callback function?

function doSomethingSomeTimes(times, callback) {
  for (let i = 0; i < times; i++) {
    callback(i)
  }
}

This is the same code as before, but now I am passing in the value of i to the callback function. So each time the callback function is called, it will get the value of i as a paramter which the callback function could choose to use.

doSomethingSomeTimes(5, (i) => {
  console.log("a new function", i)
})

This will now print out "a new function" with the value of i 5 times to the console:

> a new function 0
> a new function 1
> a new function 2
> a new function 3
> a new function 4

Real Examples

Let’s look at a more practical example, the forEach function. If we were to write forEach from scratch, it might look a little something like this:

function forEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i])
  }
}

If we call forEach and pass in an array and a callback function, it will run the callback function for each item in the array and pass in each item to the callback function.

forEach(["🤗", "🍆", "🍑"], (item) => {
  console.log(item)
})
> 🤗
> 🍆
> 🍑

forEach is a very useful function and it’s very reusable. It works with any array and any function, and it’s so useful that it’s build into JavaScript as a method on the Array object.

["🤗", "🍆", "🍑"].forEach((item) => {
  console.log(item)
})

And callback functions get more and more useful as we go. Using a callback function with the map method allows us to create a brand new array based on the original array.

const newArray = ["🤗", "🍆", "🍑"].map((item) => {
  return item+item
})

> ["🤗🤗", "🍆🍆", "🍑🍑"]

And there are many many more times when callback functions will come up in JavaScript.

Summary

Using callback functions is a great way to make code more flexible and reusable and it’s completely unavoidable in JavaScript. Not just because it’s a really useful tool, but because sometimes we can’t avoid them. When we are dealing with asynchronous code, we often have to use callback functions. And before async/await, we always had to use callback functions to handle asynchronous code. But that’s a topic for another post.

Leave a comment