Functions, callbacks, & async JavaScript
Learn how functions work, and how to manage asynchronous JavaScript code using callbacks.
In JavaScript functions are treated like any other variable. This concept is sometimes referred to as “first-class functions”. It lets us use functions in some interesting ways, and helps us manage "asynchronous" code.
Functions are variables
When you create a function in JS you are creating a normal variable:
This code creates a new variable named returnsOne
.
This is still true (and maybe more obvious) for arrow functions:
This code also creates a new variable named returnsOne
. Notice how this is similar to defining a different type of variable:
You can reference this variable the same way you would any other (by using its name):
Since functions are normal variables you can even pass them as arguments to other functions.
Mini-challenge 1
Lets try passing a function as an argument to another function. Since we're just playing around to see what happens we can write this code in the console on this page. Open up the console and try this:
Write a function named
logger
It should take one argument, then log that argument
Call
logger
with thereturnsOne
function as an argument
What does the browser print?
Functions are callable
The main distinction between a function and other types of variable is that you can call a function. You call a function by putting parentheses (round brackets) after it:
Calling a function will run the lines of code inside of it. This is useful for two reasons:
Functions let us reuse code without copy/pasting it.
Functions let us delay running code until we're ready.
Returning values
Functions need to be able to talk to each other. This is how you create a more complex program. You compose together a bunch of functions, passing the output of one into another.
The return
keyword lets us control what value we get after calling a function. Our returnsOne
function always returns a value of 1
. When you call a function the lines of code inside are run, and the function spits out its return value in place. You can then use this returned value however you like.
You can save it as a new variable:
Here you can imagine that returnsOne()
replaces itself with its return value. It's the same as if we'd written const answer = 1
directly.
You can also use the called function directly without an intermediary variable:
Here the same thing happens. returnsOne()
replaces itself with its return value. It's the same as if we'd written console.log(1)
directly.
If the function doesn't have a return
statement you'll get undefined
:
Calling or not calling a function is often a source of confusion when passing functions as arguments to other functions.
Mini-challenge 2
Open your console and recreate your
logger
function from aboveCall
logger
, but this time pass inreturnsOne()
(don't forget the parentheses)Why do we see a different value logged than before?
Edit
logger
to log the type of the value using thetypeof
operator
Inline functions
You can also define functions inline: i.e. directly as you're using them. This is a common pattern for passing functions as arguments to other functions. For example we could re-write our logger
example:
Here we're defining a new function inline at the same time that we're passing it to logger
. This is a little hard to read, which is why most developers use arrow functions for inline functions like this:
This has the same result as before, when we defined a separate returnsOne
variable and passed it by name. The main difference here is we can't re-use the function, since it only exists as an argument to logger
.
Mini-challenge 3
Inline functions are often used for event listeners in the DOM. For example this code will log wherever the user clicks on the window:
Open your console and enter the event listener above
Extract the inline function and assign it to a named variable
Pass your extracted function variable to
addEventListener
instead
The event listener should work the same whether your function is inlined or defined as a separate variable.
Callbacks
"Callback" is a scary word, but you've actually been using them the whole time. A callback is a function passed to another function as an argument. The name refers to what callbacks are usually used for: "calling you back" with a value when it's ready.
For example the addEventListener
above takes a function that it will call when the "click"
event happens. We're telling the browser "hey, call us back with the event info when that event happens".
Functions are a way to delay a block of code. Without them all our statements would run in order all in one go, and we'd never be able to wait for anything or react to user input.
Mini-challenge 4
Write a function named
one
that takes a function as a parameterIt should call that function with
1
Call your
one
function and pass in a function that logs its argument
Asynchronous callbacks
The callback above might feel a bit convoluted: why pass in a callback to access a variable from inside when we could make the one
function return 1
directly?
Callbacks make more sense when dealing with asynchronous code. Sometimes we don't have a value to return straight away.
What is "asynchronous" code?
JavaScript is a "single-threaded" language. This means things generally happen one at a time, in the order you wrote the code.
When something needs to happen out of this order, we call it "asynchronous" ("async" for short). JavaScript handles this using a "queue". Anything async gets pushed out of the main running order and into the queue. Once JS finishes what it was doing it moves on to the first thing in the queue.
setTimeout
is a built-in function that lets you run some code after a specified wait time.
It's intuitive that the above example logs 2
last, because JS has to wait a whole second before running the function passed to setTimeout
.
What's less intuitive is that the order is the same even with a timeout of 0ms.
This is because setTimeout
always gets pushed to the back of the queue—the specified wait time just tells JS the minimum time that has to pass before that code is allowed to run.
How do callbacks help?
Callbacks let us access values that may not be ready yet. Imagine ordering food in a takeaway. If you just get a pre-packaged sandwich they might be able to hand it to you straight away. This is "synchronous"—they can give you what you need then move on to the next person in the queue.
However if your food needs to be cooked you might give them your phone number, so they can text you when it's ready. This is "asynchronous"—they can move on to the next person in the queue, and "call you back" to collect your food later.
Our addEventListener
example from above can't return the click event, since it hasn't happened yet. The browser won't know where the user clicked until the click happens. So instead we pass a callback that the browser will run for us when the user clicks somewhere. It calls this callback with the event object containing the info we need.
Mini-challenge 5
Write a function
asyncDouble
that takes 2 arguments: a number and a callbackIt should use
setTimeout
to wait one secondThen it should call the callback argument with the number argument multiplied by 2
Call
asyncDouble
with10
and a callback that logs whatever it is passed. You should see20
logged after 1 second.Can you see why
asyncDouble
can't just return the doubled value?
Workshop
Let's use callbacks to make some traffic lights. Download the starter files using the command at the top of this workshop. Open challenge/index.html
in your editor.
Inside the
script
tag write a functionlight
that takes two parameters: a string and a callbackIt should wait 1 second, log the string and then call its callback parameter
Use
light
to log each colour of a traffic light sequence, in order, followed by"finished"
e.g. It should log:
with a 1 second pause before each colour
Stretch
Now we have a nested function that logs our traffic lights. However, highly nested functions can be hard to read. We can mitigate this by using a loop. Open challenge/stretch/index.html
in your editor.
Define an array,
light_sequence
containing each colour of the light sequence, ending with "finished"Modify your
light
function to include a third delay parameter and pass it to yoursetTimeout
functionUse a method such as
forEach
onlight_sequence
to pass each element of the array to thelight
functionStart by calling the
light
function with a1
second pause. What do you notice?Try and replicate the behaviour of the previous function exactly. How can you ensure the delay is kept between the log of each colour?
Last updated