Asynchronous and Synchronous JavaScript. There and back again.

Callbacks… Callbacks everywhere… Callback inside the callback, and one more inside! Asynchronous JavaScript code is a pain. Event loop is a JavaScript “bigfoot” – everyone heard about it, almost nobody knows how it works… There is multiple ways you can deal with the asynchronous action: callbacks, Promises, Observables, async-await; but which one is best? Which one should you choose for the particular scenarios?

Asynchronous and Synchronous JavaScript. There and back again.

Maciej Treder, Senior Software Development Engineer Akamai Technologies

JavaScript is a single-threaded language, meaning it can only do one thing at a time. When we need to do something time-consuming without blocking the main thread (querying APIs, loading content, etc), we use JavaScript’s event loop and callback queue mechanism.

Callbacks are a simple way to call JavaScript functions in order. But if you introduce a setTimeout with 0 delay, you can see the output change order. This demonstrates JavaScript’s execution queue.

function saySomethingNice() {
console.log(`Nice to meet you.`);
}
function greet(name) {
setTimeout(() => console.log(`Hello ${name}!`, 0));
saySomethingNice();
}
greet(`John`);

Note that setTimeout, setInterval, fetch, DOM and window are external APIs and not part of JS itself. They communicate with JavaScript using the callback queue.

To understand this we can look at the Event Loop – how the JS stack, callbacks queue and external APIs interact. By calling to an external API, the first console.log is taken out of the synchronous stack; and is intead executed in the callback queue when the stack is complete.

Maciej is going to run through making a series of REST API calls in a variety of ways.

The API has three endpoints:
/directors – a list of directors and director ID
/directors/{id}/movies – movies by director ID, with movie ID
/movies/{id}/reviews – movie reviews by movie ID

eg.
maciejtreder.github.io/asynchronous-javascript/directors
maciejtreder.github.io/asynchronous-javascript/directors/1/movies
maciejtreder.github.io/asynchronous-javascript/movies/1/reviews

What we are going to do with the API is find out which of Quentin Tarantino’s movies has the best user reviews.

The first approach is to use request API and use nested loops to dig down through the data, aggregate the scores and identify the best-rated movie.

The problem is the “pyramid of doom” – lots of nested levels of code. For three REST calls the code ends up with eight nested levels.

The next approach is to break up the code into multiple functions; and at the end of each one call the next fuction (callback pattern).

The problem is that the logical order of the code has now been reversed – you have to read bottom to top – so it’s hard to follow.

The next approach is to use Promises. This allows you to make requests from different places in the code and respond to them when they resolve.

The pitfall is that promises can only be resolved once. You also need to handle the rejection case; noting you cannot use try/catch as it will lead to an UnhandledPromiseRejectionWarning.

Next we look at promise chaining, which allows multiple actions without creating nesting issues. You can also use the catch method; and an action when all promises are resolved:

promise1
.then(val => square(val))
.then(value => console.log(value))
.catch(error => console.log(error))
.finally(() => console.log('Done'))

Another way of working with multiple promises is to combine them, using Promise.all.

It’s compact but the problem is if any of the promises are rejected, then Promise.all will reject. You can bypass this by chaining promises, with catches providing default values.

You can also use the allSettled method on the promise object (available in most browsers, but needs a shim in node). This returns an array with granular information about the status and value of each promise.

Another way to combine promises is with the race method, which gives you the fastest promise. This is useful if you are querying multiple sources but only need one of them, ie. the fastest will do.

const promise1 = Promise.resolve(3);
const square = (val) => Promise.resolve(val * val);
const promise2 = Promise.reject('Rejecting that!');
Promise.race([promise1, square(2), promise2])
.then(val => console.log(val))
.catch(error => console.log(`Failed because of: ${error}`));

This has the same pitfall as Promise.all, where one failure makes the entire set fail. Again you can chain promises with a catch statement to work around it.

But we really want the first successful promise, not just the first one to return. You can use a watchDog function to add a timeout.

So let’s use that knowledge to answer the case study question: which of Quentin’s movies has the best reviews? This is using node-fetch as we’re working in a nodejs environment.

Let’s look at the next asynchronous technique. So far we’ve covered single-action promises and multi-action callbacks; let’s take a look at something that combines them – RxJS. The basic interface is based on observables.

So how can we use RxJS to answer the case study question?

One of the big pros for using observables is that not only are they repeatable, you have plenty of operators.

So Maciej said he’s show both the synchronous and asynchronous code – the talk is called there and back again after all. So let’s forget about callback, subscribe and so on…

How can we do these these things in a more synchronous fashion? We can use async and await to make our code asynchronous again.

Async functions are equivalent to functions that return a promise with a resolve value. This adds a nesting layer and uses callbacks to work with promises.

(async () => {
let multiplier = await getMultiplier;
let result = await multiply(10, multiplier);
console.log(`Multiply result: ${result}`);
})();

vs

getMultiplier
.then(value => multiply(10, value))
.then(result => console.log(`Multiply result: ${result}`));

Let’s look at another example using RxJS – because you can use RxJS observables together with async/await.

You need to be aware of the performance pitfalls of these techniques. Observables and promises are called at different times, which changes the execution time.

Let’s compare all four techniques:

callbacks promises RxJS async-await
Asynchronous? asynchronous asynchronous asynchronous synchronous?
Repeatable? repeatable one-shot repeatable
Reusable? not reusable reusable reusable
Data manipulation not manipulatable manipulatable manipulatable
Use cases DOM events REST WebSocket Dependent operations

So after all that… what is the best movie by Quentin Tarantino, ranked by user reviews? Inglourious Basterds!

Presentation feedback link

@maciejtreder