Synchronous vs Asynchronous Callbacks
Asynchronous code in JavaScript can be confusing at best, and at worst, preventing you from landing your first job or implementing an urgent feature at work.
Just when you think you understand a program's execution order, you stumble upon asynchronous code that executes out of order and leaves you utterly confused.
To understand how asynchronous code works, it's important to know the difference between synchronous and asynchronous callbacks and be able to recognize them in your code.
Before we dive in, let's do a refresher on callback functions. If you already know what callback functions are, feel free to skip to the next section.
What is a callback function?
A callback function is a function passed as an argument to another function in order to be called from inside that function. This may sound confusing, so let's look at some code:
function printToConsole(greeting) {
console.log(greeting);
}
function getGreeting(name, cb) {
cb(`Hello ${name}!`);
}
getGreeting('Maxim', printToConsole); // Hello Maxim!
In the above example, the function printToConsole
is passed as an argument to getGreeting
. Inside getGreeting
, we call printToConsole
with a string which is then printed to the console. Because we pass printToConsole
to a function to be called from inside that function, we can say that printToConsole
is a callback function.
In practice, callback functions are often initialized anonymously and inlined in the function call. The following example is equivalent to the one above:
function getGreeting(name, cb) {
cb(`Hello ${name}!`);
}
getGreeting('Maxim', (greeting) => {
console.log(greeting);
}); // Hello Maxim!
The difference is that printToConsole
is now an anonymous callback function. Nonetheless, it's still a callback function!
Here's another example you may be familiar with:
function multiplyByTwo(num) {
return num * 2;
}
const result = [1, 2, 3, 4].map(multiplyByTwo);
console.log(result); // [2, 4, 6, 8]
Here, multiplyByTwo
is a callback function because we pass it as an argument to .map()
, which then runs the function with each item in the array.
Similar to the previous example, we can write multiplyByTwo
inline as an anonymous callback function:
const result = [1, 2, 3, 4].map((num) => {
return num * 2;
});
console.log(result); // [2, 4, 6, 8]
Order of execution
All the callbacks we've seen so far are synchronous. Before we discuss asynchronous callbacks, let's have a look at the program's order of execution first.
In what order do you think the following console.log statements are printed?
console.log('start');
function getGreeting(name, cb) {
cb(`Hello ${name}!`);
}
console.log('before getGreeting');
getGreeting('Maxim', (greeting) => {
console.log(greeting);
});
console.log('end');
If your answer was:
start
before getGreeting
Hello Maxim!
end
You got it right! The program starts at the top and executes each line sequentially as it goes to the bottom. We do a mental jump up and down when we call getGreeting
to go to the function's definition and then back to execute the callback function, but otherwise, nothing weird is happening.
Asynchronous Callbacks
Now let's have a look at asynchronous callbacks by converting getGreeting
to run asynchronously:
console.log('start');
function getGreetingAsync(name, cb) {
setTimeout(() => {
cb(`Hello ${name}!`);
}, 0);
}
console.log('before getGreetingAsync');
getGreetingAsync('Maxim', (greeting) => {
console.log(greeting);
});
console.log('end');
In what order do you think the console.log statements are printed this time around?
Go ahead, I'll wait.
.
.
.
.
.
.
.
.
.
.
The right answer is:
start
before getGreetingAsync
end
Hello Maxim!
With the addition of setTimeout, we're deferring execution of the callback function to a later point in time. The callback function will run only after the program has finished executing the code from top to bottom (even if the delay is 0ms).
The main difference between synchronous and asynchronous callbacks is that synchronous callbacks are executed immediately, whereas the execution of asynchronous callbacks is deferred to a later point in time.
This may be confusing at first, especially if you're coming from synchronous languages like PHP, Ruby or Java. To understand what's going on in the background, have a look at how the event loop works.
How can you tell if a callback is synchronous or asynchronous?
Whether a callback is executed synchronously or asynchronously depends on the function which calls it. If the function is asynchronous, then the callback is asynchronous too.
Asynchronous functions are usually the ones that do a network request, wait for an I/O operation (like a mouse click), interact with the filesystem or send a query to a database. What these functions have in common is that they interact with something outside the current program and your application is left waiting until a response comes back.
Conversely, synchronous callbacks are executed within the program's current context and there's no interaction with the outside world. You'll find synchronous callbacks in functional programming where, for example, the callback is called for each item in a collection (eg. .filter()
, .map()
, .reduce()
etc.). Most prototype methods in the JavaScript language are synchronous.
If you're not sure whether a callback function is executed synchronously or asynchronously, you can add console.log statements inside and after the callback and see which one is printed first.