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.

Transform Callbacks into Clean Async Code! 🚀

Tired of messy callback code? Download this FREE 5-step guide to master async/await and simplify your asynchronous code.

In just a few steps, you'll transform complex logic into readable, modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

Refactoring Callbacks Guide Preview

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

A Visual Guide to Refactoring Callback Functions to Promises & Async/await

Nested callbacks can be stressful. Use this technique to confidently refactor messy callbacks into clean async/await.
Read article

Run Concurrent Tasks With a Limit Using Pure JavaScript

Run asynchronous tasks with a concurrency limit without having to rely on an external library.
Read article

3 Reasons Why Async/Await Is Better Than Chaining Promises

Why use async/await when you can use promises instead? Here's why async/await excels at asynchronous JavaScript code.
Read article