Skip to content

Synchronous vs Asynchronous Callbacks

4 min read

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.

Learn how to write asynchronous code in Node.js

Write clean and easy to read asynchronous code in Node.js with this FREE 5-day email course.

Visual explanations will teach you how to decompose asynchronous code into individual parts and put them back together using a modern async/await approach. Moreover, with 30+ real-world exercises you'll transform knowledge into a practical skill that will make you a better developer.

Refactoring Callbacks, a FREE 5-day email course. 30+ real-world exercises, a visual guide and 5 days, 5 lessons.

Get Lesson 1 now 👇🏼

You'll also get tips on building scalable Node.js applications about twice a month. I respect your email privacy. Unsubscribe any time.

You might also like

Why Async/Await Inside forEach Is a Bad Idea

Using async/await inside forEach often leads to confusion. Here is how to run multiple asynchronous tasks instead.
Read article

Understanding Async & Await

Async/await can be intimidating. With a little guidance, you can write modern asynchronous JavaScript code that just works.
Read article

Using Callbacks With Async/Await

How do you make a callback-based asynchronous function work nicely with async/await? You promisify it.
Read article