I recently struggled when faced with a problem of using callbacks to handle the asynchronous flow of multiple API calls.
So I figured I’d do a deep dive into callbacks to refresh and solidify my knowledge. After all, if you want to truly understand something, teach it to someone else!
Functions as First Class Objects
Before we start talking about callbacks, we must first know that JavaScript functions are first-class objects.
Which means what? And why is that important to a discussion about callbacks?
Being a first-class object in JavaScript means that functions can have properties and methods just like any other object. Consider this:
1 2 3 4 5 6 7 8 |
function firstClass() { //DO SOMETHING } //ADD A PROPERTY TO THE FUNCTION OBJECT firstClass.answer = 42; console.log(firstClass.answer); //OUTPUT = 42 |
It also means that functions can be assigned to a variable.
1 2 3 4 5 6 |
//ASSIGN A FUNCTION TO THE firstClass VARIABLE let firstClass = function(answer) { console.log(answer); } console.log(firstClass(42)); //OUTPUT = 42 |
More importantly for our discussion on callbacks, however, is that because functions are first-class objects, they can be passed to (and returned from) other functions.
1 2 3 4 5 6 7 8 9 10 11 12 |
//CREATE FUNCTION THAT TAKES A CALLBACK function logCallback(answer) { console.log(answer()); } //DEFINE THE CALLBACK function firstClassFunction() { return 42; } //CALL THE ORIGINAL FUNCTION WITH OUR CALLBACK logCallback(firstClassFunction); //OUTPUT = 42 |
This last point, passing functions as arguments to other functions, is the foundation of callbacks.
A callback is simply a function that is passed to another function as an argument, that can then be used (executed) inside that other function.
The previous example used a named function (answer) as the callback. You can (and probably have) also use anonymous functions as callbacks. This is common when passing callbacks to the native prototype methods for arrays, objects and strings.
Consider the following:
1 2 3 4 5 6 7 |
let numArr = [1,2,3]; numArr.forEach((element) => { console.log(element * 2)}); //2 //4 //6 |
Here we used an anonymous arrow function as our callback to output twice the value of each element.
Now that we know what callbacks are, let’s talk about their use in asynchronous JavaScript.
Understanding ASYNCHRONY
No, this isn’t what we’re talking about, but the word ‘asynchony’ makes me think of this album….. Yeah, I know, I’m old…
Before we start talking about controlling asynchronous flow we must first understand the nature of the JavaScript event loop and the fact that it is single threaded. Digging deep into the JavaScript engine is beyond the scope of this post, but understanding the single threaded nature of the event loop is critical to understanding why we need asynchronous code.
For now, think of the event loop as a queue (first in, first out) that executes snippets of your JS code one by one and in the order they were placed in the queue.
Now think of an Ajax request that we might use to get some data from an API. That Ajax request is costly (in time). If we place it in the event loop and run it synchronously it will block everything that comes after it while it waits for a response. This affects performance and causes people to leave your site!
In his You Don’t Know JS series, Kyle Simpson describes asynchrony as the difference between now and later, as opposed to parallel, which describes two processes that are executed independent of each other (but possibly at the same time).
This now and later concept is where callback functions come in. Rather than running that Ajax request from start to finish, blocking all other code execution while we wait, we pass it a callback.
In this scenario, the JS engine can make the Ajax call (now), continue with the event loop, and once the Ajax call returns (later) pass the callback, along with any data it needs from the response, back into the event loop.
Async in action
Let use an example to show what we’re talking about. For the remaining examples, I’m going to use setTimeout() to simulate async functions, but you can think of it as time spent making an Ajax request.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function first() { console.log("first"); } function second() { console.log("second"); } function delay(callback){ window.setTimeout(callback, 500); } delay(first); second(); //second //first |
This example shows asynchronous control of the event loop. Let’s dig deeper in case this isn’t clear.
We’ve defined three functions first, second and delay.
first and second merely log “first” and “second” to the console so we can tell when they’re executed.
delay takes a callback, which it passes to setTimeout(). setTimeout() sets a timer, then calls the function it was passed – in this case, it will call callback after 500 milliseconds.
After the three function definitions, we call delay(first), which puts the function delay() in our event loop. Upon execution, delay sets a timer for 500 ms, after which, it will call its callback (in this case first()).
Because the timer in setTimeout() is non-blocking, or asynchronous, the event loop is now free to move on to the next snippet of JS, which is our function call to second().
When second is executed, it logs “second” to the console, which is why we see “second” logged to the console before we see “first”, even though delay(first) was called before second().
And finally, after the 500 ms timer elapses, first() is placed in our event loop and executed, which is when “first” shows up in our console.
I’m hoping the example above was simple enough that you could figure it out on your own, otherwise, maybe my long winded explanation helped.
Fun with callbacks and closure
OK, now that we’re pros with callbacks, lets have a little fun with them!
We’re going to use callbacks and closure to set up a four lane race. Granted, it’s only a race in the sense that we’re randomizing the setTimout() time, but it’s fun nonetheless…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/* *WHEN EXECUTED, racer() WILL RETURN A FUNCTION THAT HAS CLOSURE OVER THE 'lane' VALUE *THE RETURNED FUNCTION TAKES A CALLBACK AS AN ARGUMENT *THIS CALLBACK IS THEN EXECUTED WITH THE LANE VALUE AS AN ARGUMENT AT A RANDOM TIME BETWEEN 0 AND 5 SECONDS *WE'LL CALL THIS FUNCTION WITH 4 DIFFERENT LANE VALUES TO SIMULATE A 'RACE' */ function racer(lane) { return (place) => { window.setTimeout(() => { place(lane); }, Math.random() * 5000); }; } function runRace(one, two, three, four, printResults) { let placeOrder = [0, 0, 0, 0]; //TO STORE RESULTS //START RACE BY EXECUTING FIRST 4 ARGS (FUNCTIONS) one(finish); two(finish); three(finish); four(finish); //FUNCTION TO SERVE AS CALLBACK - SIMULATES THE RACER 'FINISHING' function finish(laneValue) { //PUT laneValue IN THE NEXT AVAILABLE SPOT IN placeOrder let i=0; while(placeOrder[i] != 0) { i++; } placeOrder[i] = laneValue; //CHECK IF RACE IS OVER, IF SO, PRINT RESULTS if(i == placeOrder.length-1){ printResults(...placeOrder); } } } function showResults(first, second, third, fourth) { console.log(`The race order was ${first}, ${second}, ${third}, ${fourth}`); } ////////////// START THE RACE //////////////////// /* *CALL THE RACER FUNCTION WITH THE LANE NUMBER (1 THRU 4) *ARGS 1 THRU 4 IN runRace ARE NOW FUNCTIONS WITH CLOSURE OVER THEIR LANE NUMBER */ runRace(racer(1), racer(2), racer(3), racer(4), showResults); |
Hopefully, the comments in the code clear up how the “race” is being run. If you have any questions, feel free to leave them below.
-Jeremy