Event Loop, Callback, Message Queue, Job Queue, Promises, Async/Await

JavaScript — Event Loop, Callback

Some basic terms in JavaScript

Quang Trong VU
Old Dev

--

The Event Loop

The Event Loop is one of the most important aspects to understand about JavaScript.

JavaScript code runs in a single thread. There is just one thing happening at a time. This helps your program does not worry about concurrency issues. You just need to pay attention to how to write your code and avoid anything that could block the thread, like synchronous network calls or infinite loops.

In general, in most browsers, there is an event loop for every browser tab, to make every process isolated and avoid a web page with infinite loops or heavy processing to block your entire browser. The environment manages multiple concurrent event loops, to handle API calls for example. Web Workers run in their own event loop as well.

Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. In addition, they can perform I/O using XMLHttpRequest (although the responseXML and channel attributes are always null). Once created, a worker can send messages to the JavaScript code that created it by posting messages to an event handler specified by that code (and vice versa).— MDN

1. Blocking the event loop

If JavaScript code takes too long to return back control to the event loop, It will block the execution of any anther JavaScript code in the page, even block the UI thread, the consequence is the user cannot click around, scroll the page, and so on. Almost the I/O primitives in JS are non-blocking. Network requests, Node.js filesystem operations, and so on. Being blocking is the exception, and this is why JS is based so much on callbacks, and more recently on promises and async/await.

2. The call stack

The call stack is a LIFO queue (Last In First Out). The event loop continuously checks the call stack to see if there’s any function that needs to run. While doing so, it adds any function call it finds, to the call stack, and executes each one in order. The error stack trace, you see on the debugger or in the browser console, is looked upon the call stack.

3. A simple event loop explanation

const bar = () => console.log('bar')const baz = () => console.log('baz')const foo = () => {
console.log('foo')
bar()
baz()
}foo()

output

foo
bar
baz

At this point the call stack looks like this:

Call Stack

The event loop on every iteration looks if there’s something in the call stack, and executes it until the call stack is empty.

Event Loop Iterations

4. Queuing function execution

Let’s see how to defer a function until the stack is clear. You can use setTimeout(() => {}, 0) to call a function but execute it once every other function in the code has executed.

const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()

output

foo
baz
bar

the call stack looks like this

Call Stack

the event loop iterations

Event Loop Iteration

Why is this happening?

5. The Message Queue

When setTimeout() is called, the Browser or Node.js start the timer. Once the timer expires, in this case immediately as we put 0 as the timeout, the callback function is put in the Message Queue.

The Message Queue is also where user-initiated events like click or keyboard events, or fetch responses are queued before your code has the opportunity to react to them. Or also DOM events like onLoad.

The loop gives priority to the call stack, and it first processes everything it finds in the call stack, and once there’s nothing in there, it goes to pick up things in the event queue.

http://latentflip.com/loupe

We don’t have to wait for functions like setTimeout(), fetch or other things to do their own work because they are provided by the browser, and they live on their own threads. For example, if you set the setTimeout timeout to 2 seconds, you don’t have to wait 2 seconds — the wait happens elsewhere.

6. ES6 Job Queue

ECMAScript 2015 (ES6) introduced the concept of the Job Queue, which is used by Promises (also introduced in ES6). It’s a way to execute the result of an async function AS SOON AS possible, RATHER THAN being put at the end of the call stack.

Promises that resolve before the current function ends will be executed right after the current function.

const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('should be right after baz, before bar')
).then(resolve => console.log(resolve))
baz()
}
foo()

This prints

foo
baz
should be right after baz, before bar
bar

That’s a big difference between Promises (and Async/Await, which is built on Promises) and Plain Old Asynchronous functions through setTimeout() or other platform APIs. For example:
- setTimeout() method results are put to Message Queue and are executed after clear Call Stack.
- Promises/Async method results are put to Job Queue and are executed right after the Current Running function on Call Stack, not need to wait to empty Call Stack.

Asynchronous Programming and Callbacks

Default, JavaScript is synchronous and is single-threaded. This means that code cannot create new threads and run in parallel. Let’s learn what asynchronous code means and how it looks.

1. Asynchronicity in Programming Languages

Programs internally use interrupts, a signal that’s emitted to the processor to gain the attention of the system. Normally a program is asynchronous, it halts their execution until they need attention, and the computer can execute other things in the meantime. When a program is waiting for a response from the network, it cannot halt the processor until the request finishes.

C, Java, C#, PHP, Go, Ruby, Swift, Python they all are synchronous by default. Some of them handle async by using threads, spawning a new process.

2. JavaScript

JavaScript is synchronous by default and is single-threaded. But JavaScript was born inside the browser, its main job, in the beginning, was to respond to user actions, like onClick, onMouseOver, onChange, onSubmit, and so on. How could it do this with a synchronous programming model?

The answer was in its environment. The browser provides a way to do it by providing a set of APIs that can handle this kind of functionality.

More recently, Node.js introduced a non-blocking I/O environment to extend this concept to file access, network calls, and so on.

3. Callbacks

You can’t know when a user is going to click a button, so what you do is, you define an event handler for the click event. This event handler accepts a function, which will be called when the event is triggered.

document.getElementById('button').addEventListener('click', () => {
// item clicked
})

This is the so-called callback.

A callback is a simple function that’s passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions).

It’s common to wrap all your client code in a load event listener on the window object, which runs the callback function only when the page is ready.

window.addEventListener('load', () => {
// Window loaded
// do what you want
})

Callbacks are used everywhere, not just in DOM events.

One common example is by using times:

setTimeout(() => {
// run after 2 seconds
})

XHR requests also accept a callback. In this example, by assigning a function to a property that will be called when a particular event occurs (in this case, the state of request changes).

const xhr = new XMLHttpRequest() 
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()

4. Handling errors in Callbacks

How do you handle errors with callback? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks.

If there is no error, the object is null. If there is an error, it contains some description of the error and other information.

fs.readFile('/file.json', (err, data) => {
if (err !== null) {
// handle error
console.log(err)
return
}
// no errors, process data
console.log(data)
})

5. The problem with Callbacks

Callbacks are great for simple cases!

However, every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly

window.addEventListener('load', () => {
document.getElementById('button'.addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
})
}, 2000)
})
})

This is just a simple 4-levels code, but I’ve seen much more levels of nesting and it’s not fun.

How do we solve this?

6. Alternatives to Callbacks

Starting with ES6, JavaScript introduced several features that help us with asynchronous code that does not involve using callbacks:

  • Promises (ES6)
  • Async/Await (ES8)

6.1 Promises

Promises are one way to deal with asynchronous code in JavaScript, without writing too many callbacks in your code.

A promise is commonly defined as a proxy for a value that will eventually become available.

Although being around for years, they have been standardized and introduced in ES6, and now they have been superseded in ES8 by Async function.

Async functions use the promises API as their building block, so understanding them is fundamental even if in newer code you’ll likely use async functions instead of promises.

  • How Promises work, in brief

Once a promise has been called, it will start in pending state. This means that the caller function continues the execution, while it waits for the promise to do its own processing, and give the caller function some feedback.

At this point, the caller function waits for it to either return the promise in a resolved state, or in a rejected state, but the function continues its execution while the promise does its work.

  • Which JS APIs use promises?

In addition to your own code and libraries code, promises are used by standard modern Web APIs such as: the Battery API, the Fetch API, Service Workers.

It’s unlikely that in modern JavaScript you’ll find yourself not using promises, so let’s start driving right into them

  • Creating a promise

The Promise API exposes a Promise constructor, which you initialize using new Promise()

let done = trueconst isItDoneYet = new Promise(
(resolve, reject) => {
if (done) {
const workDone = 'Here is the thing I built'
resolve(workDone)
} else {
const why = 'Still working on something else'
reject(why)
}
}
)

As you can see the promise checks the done global constant, and if that’s true, we return a resolved promise, otherwise a rejected promise, otherwise a rejected promise.

Using resolve and reject we can communicate back a value, and in the above case we just return a string, but it could be an object as well.

  • Consuming a promise

In the last section, we introduced how a promise is created.

Now let’s see how the promise can be consumed or used.

const isItDoneYet = new Promise(
// ...
)
const checkIfItsDone = () => {
isItDoneYet
.then((ok) => {
console.log(ok)
})
.catch((err) => {
console.error(err)
})
}

Running checkIfItsDone() will execute the isItDoneYet() promise and will wait for it to resolve, using the then callback, and if there is an error, it will handle it in the catch callback.

  • Chaining promises

A promise can be returned to another promise, creating a chain of promises.

A great example of chaining promises is given by the Fetch API, a layer on top of the XMLHttpRequest API, which we can use to get a resource and queue a chain of promises to execute when the resource is fetched.

The Fetch API is a promise-based mechanism, and calling fetch() is equivalent to defining our own promise using new Promise().

  • Example of chaining promises
const status = (response) => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
}
return Promise.reject(new Error(response.statusText))
}
const json = (response) => response.json()fetch('/todos.json')
.then(status)
.then(json)
.then((data) => { console.log('Request succeeded with JSON response', data) })
.catch((error) => { console.log('Request failed', error) })

In this example, we call fetch() to get a list of TODO items from the todos.json file found in the domain root, and we create a chain of promises.

Running fetch() returns a response, which has many properties, and within those we reference:

~ status: a numeric value representing the HTTP status code
~ statusText: a status message, which is OK if the request succeeded.

response also has a json() method, which returns a promise that will resolve with the content of the body processed and transformed into JSON.

So given those premises, this is what happens: the first promise in the chain is a function that we defined, called status(), that checks the response status and if it’s not a success response (between 200 and 299), it rejects the promise.

This operation (reject) will cause the promise chain to skip all the chained promises listed and will skip directly to the catch() statement at the bottom, logging the Request failed text along with the error message.

If that succeeds instead, it calls the json() function we defined. Since the previous promise, when successful, returning the response object, we get it as an input to the second promise.

In this case, we return the data JSON processed, so the third promise receives the JSON directly:

.then((data) => {
console.log('Request succeeded with JSON response', data)
})

and we simply log it to the console.

  • Handling errors

In the example, in the previous section, we had a catch that was appended to the chain of promises.

When anything in the chain of promises fails and raises an error or rejects the promise, the control goes to the nearest catch() statement down the chain.

new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch((err) => { console.error(err) })
// ornew Promise((resolve, reject) => {
reject('Error')
})
.catch((err) => { console.error(err) })
  • Cascading errors

If inside the catch() you raise an error, you can append a second catch() to handle it, and so on.

new Promise((resolve, reject) => {
throw new Error('Error 1')
})
.catch((err) => { throw new Error('Error 2') })
.catch((err) => { console.error(err) })
  • Orchestrating promises

Promise.all() and Promise.race()

If you need to synchronize different promises, Promise.all() helps you define a list of promises, and execute something when they are all resolved.

For example:

const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2]).then((res) => {
console.log('Array of results', res)
})
.catch((err) => {
console.error(err)
})

The ES6 destructuring assignment syntax allows you to also do

Promise.all([f1, f2]).then(([res1, res2]) => {
console.log('Results', res1, res2)
})

You are not limited to using fetch of course, any promise is good to go.

Promise.race() runs when the first of the promises you pass to it resolves, and it runs the attached callback just once, with the result of the first promise resolved.

For example:

const first = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then((result) => {
console.log(result) // second
})
The ‘second’ is finished first

6.2 Async and Await

Async functions are a combination of promises and generators, and basically, they are a higher level abstraction over promises. Again, Async/Await is built on Promises

  • Why were Async/Await introduced?

They reduce the boilerplate around Promises, and the “don’t break the chain” limitation of chaining promises.

When Promises were introduced in EF6, they were meant to solve a problem with asynchronous code, and they did, but over 2 years from EF20015(ES6) to ES2017(ES8), it was clear that Promises could not be the final solution.

Promises were introduced to solve the famous callback hell problem, but they introduced complexity on their own, and syntax complexity.

They were good primitives around which a better syntax could be exposed to the developers, so when the time was right we got Async functions.

They make the code look like it’s synchronous, but it’s asynchronous and non-blocking behind the scenes.

  • How it works

An async function returns a promise, like in this example:

const doSomethingAsync = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000
)}
)}

When you want to call this function you prepend await , and the calling code will stop until the promise is resolved or rejected. One caveat: the client function must be defined as async. Here’s an example:

const doSomething = async () => {
console.log(await doSomethingAsync())
}
  • A quick example
const doSomethingAsync = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000)
})
}
const doSomething = async () => {
console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
  • Promise all the things

Prepending the async keyword to any function means that the function will return a promise. Even if it’s not doing so explicitly, it will internally make it return a promise. This is why this code is valid:

const foo = async () = {
return 'test'
}
foo().then(alert) // this will alert 'test'

and it’s the same as:

const baz = async () = {
return Promise.resolve('test')
}
baz().then(alert) // this will alert 'test'
  • The code is much simpler to read
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => response.json()) // parse JSON
.then(users => users[0]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => response.json()) // parse JSON
}
getFirstUserData()

and using Async function

const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
const users = await response.json() // parse JSON
const user = users[0] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
const userData = await user.json() // parse JSON
return userData
}
getFirstUserData()
  • Multiple async functions in series

Async functions can be chained very easily, and the syntax is much more readable than with plain promises

const foo = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchLevelOne = async () => {
const baz = await foo()
return baz + ' and Level One'
}
const watchLevelTwo = async () => {
const bar = await watchLevelOne()
return bar + ' and Level Two'
}
watchLevelTwo().then((res) => {
console.log(res)
})
  • Easier debugging

Debugging Promises are hard because the debugger will not step over the asynchronous code.

Async/Await makes this very easy because the compiler it’s just like synchronous code.

Conclusion

Till now I’ve brief about The Event Loop with some members: Call Stack, Queuing (Message Queue for old asynchronous function type: setTimeout, Job Queue for new asynchronous function type: Promise/Async). Message Queue is checked after Call Stack is empty, Job Queue is checked right after the current running function (in Call Stack) is finished. And the second topic about Callback function with traditional style (setTimeout), and Promises style, and finally is Async/Await style.

References

http://latentflip.com/loupe

--

--

Quang Trong VU
Old Dev
Editor for

Software Developer — Life’s a journey — Studying and Sharing