Asynchronous Callbacks in JavaScript

Naveera A.
—The Problem
How do callbacks work in JavaScript?
Let’s say we have the following code:
function first() { console.log('First'); } function second() { console.log('Second'); } function third() { console.log('Third'); }
We call the functions in order like so:
first(); second(); third();
And we get the following output:
First Second Third
Let’s add a setTimeout method in the second function, like so:
function first() { console.log('First'); } function second() { setTimeout(() => { console.log('Second'); }, 0); } function third() { console.log('Third'); }
Now if we call the functions in order as before, we get a different result:
First Third Second
Why does the second function run after the third function, even though the timer in the setTimeout function is set to 0?
The Solution
To understand why we are getting the unexpected output, we first need to understand the JavaScript event loop.
Since JavaScript is a single-threaded programming language, it can only process one statement at a time. To handle the asynchronous operations, like the setTimeout() method, JavaScript uses a concept called the event loop.
According to the documentation:
JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.
The event loop keeps track of the order of execution using a stack and a message queue.
You can think of the stack as the execution area, where each function is added in order. The queue is a waiting area for the asynchronous functions.
Let’s take a high-level look at how the event loop will execute our three functions.
Functions Without a Callback (or Asynchronous Code)
For the three functions without any asynchronous code, the browser will do the following:
Add
first()to the stack.- Run
first()which will logFirstto the console. - Remove
first()from the stack.
- Run
Add
second()to the stack.- Run
second()which will logSecondto the console. - Remove
second()from the stack.
- Run
Add
third()to the stack.- Run
third()which will logThirdto the console. - Remove
third()from the stack.
- Run
Check the queue for messages (functions waiting to be processed).
- Queue is empty.
Execution cycle complete.
And we will get the following output, as expected:
First Second Third
Functions With a Callback (or Asynchronous Code)
When we add the setTimeout() method to the second function, our execution steps look like the following:
Add
first()to the stack.- Run
first()which will logFirstto the console. - Remove
first()from the stack.
- Run
Add
second()to the stack.- Run
second()- Add
setTimeout()to the stack on top ofsecond(). - Run
setTimeoutAPI, which will start a timer. At the end of the timer, the anonymous function will be added to the message queue. - Remove
setTimeout()from the stack.
- Add
- Remove
second()from the stack.
- Run
Add
third()to the stack.- Run
third()which will logThirdto the console. - Remove
third()from the stack.
- Run
Check the queue for messages.
- Find the anonymous function from
setTimeout()
- Find the anonymous function from
Add the anonymous function to the stack.
- Run the anonymous function which will log
Secondto the console. - Remove the anonymous function from the stack.
- Run the anonymous function which will log
Check the queue again for messages.
- Queue is empty.
Execution cycle complete.
And we will get the following output:
First Third Second
The important thing to remember is that the timer in the setTimeout() method does not set the time after which the code will execute. It sets the time after which the event loop will add the anonymous function to the queue.
If we were working with an external web API, the anonymous function would be added to the queue after the API has returned its data.
The timer, or any other asynchronous code, cannot add the anonymous function directly to the stack at the end of its completion as it would interrupt the currently running function.
And the event loop will execute any function in the message queue only after executing all the top-level functions.
If we want to delay the execution of the third function until the second function has been completed, we can use the third function as a callback to the second function by passing it as an argument to the second function, like so:
function first() { console.log('First'); } function second(cb) { setTimeout(() => { console.log('Second'); // Execute the callback function cb(); }, 0); } function third() { console.log('Third'); } first(); second(third);
When we run the above code, we will get the desired output:
First Second Third
- YoutubeHow Sentry.io saved me from disaster (opens in a new tab)
- ResourcesImprove Web Browser Performance - Find the JavaScript code causing slowdowns (opens in a new tab)
- SentryJavascript Error Monitoring & Tracing (opens in a new tab)
- ResourcesJavaScript Frontend Error Monitoring 101 (opens in a new tab)
- Syntax.fmListen to the Syntax Podcast (opens in a new tab)
- Listen to the Syntax Podcast (opens in a new tab)
![Syntax.fm logo]()
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODES
Considered “not bad” by 4 million developers and more than 150,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
