rawjs.xyz

19.08.2023

Callbacks in Javascript

Callbacks, the primary way asynchronous data travels around functions in JS. Pretty standard stuff if you ask me. Let's use modern examples to help you understand what a callback is.

1addEventListener(event => {
2  // do something with the event.
3})

You've probably seen this pattern in a lot of places. If we were to implement something similar, it'd look like this

1function addEventListener(listener) {
2  const event = {}
3  // listener being the function passed by the user(developer)
4  listener(event)
5}

All good till now? Cool.

That's a callback. listener here is the callback. It's pretty much what the name suggest, you are calling a function which requested that it should be called.

Let's go through a few more examples of the same pattern

Common examples

DOM event handlers

1button.addEventListener("click",(e)=.{
2    // do something on a button click
3})
4
5input.addEventListener("change",(e)=.{
6    // do something with e.target.value
7})

Plugin Hooks

1export default plugin = {
2  setup(builder) {
3    builder.onWrite(chunk => {
4      // do something with the chunk
5    })
6  },
7}

Promises

1someAsyncOp()
2  // then takes a callback signifying to call it when `someAsyncOp` has finished
3  .then(data => {
4    // do something with data here
5  })

Why ?

But why do we need this? What advantage does it have?

Function closures prevent you from accessing another function's data so something like the code snippet below, doesn't work.

1function funcOne() {
2  const a = 1
3}
4
5function funcTwo() {
6  console.log(a)
7}

a doesn't exist in funcTwo; if you ever call funcTwo(), you would get an Error saying a is not defined.

Now there's a few options.

First: Move a to a scope above the functions and then call them to achieve the behaviour.

 1let a
 2function funcOne() {
 3  a = 1
 4}
 5function funcTwo() {
 6  console.log(a)
 7}
 8
 9funcOne()
10funcTwo()

Which is fine if that's what we wish to achieve, but there's a tiny issue. The execution of the program is tightly coupled to the sequence of the functions and makes it very easy to break something like this since neither of the functions are aware of the other one's dependency.

To solve that issue, we can make a slight modification to it.

 1function funcOne() {
 2  const a = 1
 3  funcTwo(a) // => Call the function here
 4}
 5
 6// add it as a parameter to the function being called
 7function funcTwo(a) {
 8  console.log(a)
 9}
10
11// singular call
12funcOne()

At this point, funcOne knows that funcTwo needs to be called to move forward and is also aware of the dependency of funcTwo is the variable a but, we still have a problem. This in itself isn't a problem and you'll see this pattern in a lot of programs.

This pattern leads to a problem when you wish to call funcOne in multiple places and not trigger funcTwo everytime. You've created an event handler that's tied to the function which isn't always nice. What we can do is, change funcOne to tell the user that I need an event handler since I cannot continue without one.

We can do this by changing the code to look like this.

 1// add a new parameter that'll be called after initializing `a`
 2function funcOne(handler) {
 3  const a = 1
 4  handler(a)
 5}
 6
 7function funcTwo(a) {
 8  console.log(a)
 9}
10
11function funcThree(a) {
12  console.warn(a)
13}
14
15// call `funcOne` with the function it needs to call after it's done
16funcOne(funcTwo)
17
18// somewhere else in the code
19funcOne(funcThree)

Hopefully that helps your understand what callbacks are, let us know if there's a topic you'd like to understand.