Written on December 3, 2021
Javascript asynchronous code execution flow
Javascript is a wonderful language and really provides many tools for code execution. One of the strengths of js is how it handles Asynchronous (async) code.
Rather than blocking the main execution thread, async code gets pushed to the event queue that fires after all other code executes. It can, however, be difficult for beginners to follow async code. This article aims to clarify all of that.
So the most basic example of asynchronous code in js are setTimout
and setInterval
.
setTimout
executes a function after a certain amount of time passes. It accepts a callback function and time(in milliseconds) as the second argument.
console.log('a')
setTimeout(function () {
console.log('c')
}, 100)
setTimeout(function () {
console.log('d')
}, 100)
setTimeout(function () {
console.log('e')
}, 100)
console.log('b')
/* The output will be
a
b
c
d
e
*/
Here we can see that the output matches our expectation.
The execution of code happened as following,
console.log('a')
setTimeout
to event queue.console.log('b')
which it executed blocking-ly.The event loop is a queue of callback functions. When an async function executes, the callback function is pushed into the queue. The javascript engine doesn't start processing the event loop until the code after an async function has executed. This allows us to see that our js code is not multi-threaded, even though it appears so.
The Event loop is a FIFO queue, meaning the callback function will execute in the order they were added onto the queue. And using node allows us to use this asynchronous behavior on the backend as well.
This is the main method any async code is executed. Async programming lends itself to what's commonly referred to as "callback hell". Because virtually all async functions in JavaScript use callbacks, performing multiple sequential async functions result in many nested callbacks--resulting in hard to read code.
const fs = require('fs')
fs.exists('index.js', function () {
fs.readFile('index.js', 'utf8', function (err, contents) {
contents = someFunction(contents) // do something with contents
fs.writeFile('index.js', 'utf8', function () {
console.log('whew! Done finally...')
})
})
})
console.log('executing...')
Nested callbacks can get really nasty, but there are several solutions to this style of coding.
An easy solution that cleans nested callbacks is simply avoiding nesting more than two levels. Instead of passing anonymous functions to the callback arguments, pass a named function, which is a feature that javascript provides us.
var fromLatLng, toLatLng
// And lastly this function will execute
var routeDone = function (e) {
console.log("ANNNND FINALLY here's the directions...")
// do something with e
}
// Secondly this function will execute
var toAddressDone = function (results, status) {
if (status == 'OK') {
toLatLng = results[0].geometry.location
map.getRoutes({
origin: [fromLatLng.lat(), fromLatLng.lng()],
destination: [toLatLng.lat(), toLatLng.lng()],
travelMode: 'driving',
unitSystem: 'imperial',
callback: routeDone,
})
}
}
// First this function will execute
var fromAddressDone = function (results, status) {
if (status == 'OK') {
fromLatLng = results[0].geometry.location
GMaps.geocode({
address: toAddress,
callback: toAddressDone,
})
}
}
// Initializing function
GMaps.geocode({
address: fromAddress, // address object is defined globally
callback: fromAddressDone,
})
While using named functions is a neat concept that is widely used, but keeping track of named function variables can get quite daunting, quite fast.
And there's no guarantee that those variables can't be overridden by other parts of our program.
For this reason and where we are sure about the flow of the data. We can use promises, which allows us to chain functions to achieve a desired state in our application.
Let's look at how we can initialize a promise
let promise = new Promise(function (resolve, reject) {
// the executor function is executed
// automatically when the promise is constructed
// after 1 second signal that the job is
// done with the result "done"
setTimeout(() => resolve('done'), 1000)
})
Now the part where we chain the consumer functions
A Promise object serves as a link between the executor (the “producing code”) and the consuming functions, which will receive the result or error. Consuming functions can be registered (subscribed) using methods .then
, .catch
and .finally
.
The most important, fundamental one is .then
.
promise.then(
function (result) {
/* handle a successful result */
},
function (error) {
/* handle an error */
}
)
The first argument of .then
is a function that runs when the promise is resolved, and receives the result.
The second argument of .then
is a function that runs when the promise is rejected, and receives the error.
If we’re interested only in errors, then we can use null
as the first argument: .then(null, errorHandlingFunction)
. Or we can use .catch(errorHandlingFunction)
, which is exactly the same:
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Whoops!')), 1000)
})
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert) // shows "Error: Whoops!" after 1 second
The call .catch(f)
is a complete analog of .then(null, f)
, it’s just a shorthand.
Just like there’s a finally
clause in a regular try {...} catch {...}
, there’s finally
in promises.
The call .finally(f)
is similar to .then(f, f)
in the sense that f
always runs when the promise is settled: be it resolve or reject.
finally
is a good handler for performing cleanup, e.g. stopping our loading indicators, as they are not needed anymore, no matter what the outcome is.
Like this:
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve/reject */
})
// runs when the promise is settled
// doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always
// stopped before we process the result/error
.then(result => show result, err => show error)
That said, finally(f)
isn’t exactly an alias of then(f,f)
though. There are few subtle differences:
A finally
handler has no arguments. In finally
we don’t know whether the promise is successful or not. That’s all right, as our task is usually to perform “general” finalizing procedures.
A finally
handler passes through results and errors to the next handler.
For instance, here the result is passed through finally
to then
:
new Promise((resolve, reject) => {
setTimeout(() => resolve('result'), 2000)
})
.finally(() => alert('Promise ready'))
.then((result) => alert(result)) // <-- .then handles the result
And here there’s an error in the promise, passed through finally
to catch
:
new Promise((resolve, reject) => {
throw new Error('error')
})
.finally(() => alert('Promise ready'))
.catch((err) => alert(err)) // <-- .catch handles the error object
That’s very convenient, because finally
is not meant to process a promise result. So it passes it through.
Events are another solution to communicate when async callbacks finish executing. An object can become an emitter and publish events that other objects can listen for. This type of eventing is called the observer pattern.
The Event Loop is a queue of callback functions.
Browser JavaScript execution flow, as well as in Node.js, is based on an event loop.
Understanding how event loop works is important for optimizations, and sometimes for the right architecture.
In this chapter we first cover theoretical details about how things work, and then see practical applications of that knowledge.
The event loop concept is very simple. There’s an endless loop, where the JavaScript engine waits for tasks, executes them and then sleeps, waiting for more tasks.
The general algorithm of the engine:
That’s a formalization for what we see when browsing a page. The JavaScript engine does nothing most of the time, it only runs if a script/handler/event activates.
Examples of tasks:
<script src="...">
loads, the task is to execute it.mousemove
event and execute handlers.setTimeout
, the task is to run its callback.Tasks are set – the engine handles them – then waits for more tasks (while sleeping and consuming close to zero CPU).
It may happen that a task comes while the engine is busy, then it’s enqueued.
The tasks form a queue, so-called “macrotask queue” (v8 term):
For instance, while the engine is busy executing a script
, a user may move their mouse causing mousemove
, and setTimeout
may be due and so on, these tasks form a queue, as illustrated on the picture above.
Tasks from the queue are processed on “first come – first served” basis. When the engine browser is done with the script
, it handles mousemove
event, then setTimeout
handler, and so on.
So far, quite simple, right?
Two more details:
Along with macrotasks, described in this chapter, there are microtasks, mentioned in the chapter Microtasks.
Microtasks come solely from our code. They are usually created by promises: an execution of .then/catch/finally
handler becomes a microtask. Microtasks are used “under the cover” of await
as well, as it’s another form of promise handling.
There’s also a special function queueMicrotask(func)
that queues func
for execution in the microtask queue.
Immediately after every macrotask, the engine executes all tasks from microtask queue, prior to running any other macrotasks or rendering or anything else.
For instance, take a look:
setTimeout(() => alert('timeout'))
Promise.resolve().then(() => alert('promise'))
alert('code')
What’s going to be the order here?
code
shows first, because it’s a regular synchronous call.promise
shows second, because .then
passes through the microtask queue, and runs after the current code.timeout
shows last, because it’s a macrotask.The richer event loop picture looks like this (order is from top to bottom, that is: the script first, then microtasks, rendering and so on):
All microtasks are completed before any other event handling or rendering or any other macrotask takes place.
That’s important, as it guarantees that the application environment is basically the same (no mouse coordinate changes, no new network data, etc) between microtasks.
If we’d like to execute a function asynchronously (after the current code), but before changes are rendered or new events handled, we can schedule it with queueMicrotask
.
A more detailed event loop algorithm (though still simplified compared to the specification):
To schedule a new macrotask:
setTimeout(f)
.That may be used to split a big calculation-heavy task into pieces, for the browser to be able to react to user events and show progress between them.
Also, used in event handlers to schedule an action after the event is fully handled (bubbling done).
To schedule a new microtask
queueMicrotask(f)
.There’s no UI or network event handling between microtasks: they run immediately one after another.
So one may want to queueMicrotask
to execute a function asynchronously, but within the environment state.
In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely tied with the “event loop” and “macrotasks”. As these have no direct relation to promises, they are covered in another part of the tutorial, in the article Event loop: microtasks and macrotasks.