Javascript Asynchronous Programming

This post will briefly discuss the mechanism of JavaScript runtime, and three ways to implement asynchronous programming in JavaScript.

Event Loop

Although it is not strictly correct that JavaScript is a single-threaded laungage because either in Browser or Nodejs there might be multiple threads running, JavaScipt script is truly executed in a single thread in runtime, which means it can only do one thing at a time. In intuition then, how it can be asynchronous? The key is Event Loop, a never-bloking and endless loop which keeps waiting -> checking -> executing -> waiting tasks from a message queue. It looks like this:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

In JavaSsript, whenever a function runs, it is not executed immediately until all other previous functions are completed. JavaScript uses a message queue to store tasks to be processed. In Browser, for example, when you click a button, the event listener just put the function triggered by the click to the message queue. Only after all functions pre-enqueued are processed, this functions will be executed. So the answer to the counter-intuition is: JavaScript implements asynchroncity by manipulating the order of codes processing.

Begin with a simple example:

1
2
3
4
5
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);

Output:

1
2
3
1
3
2

Above is a very simple asynchronous example. setTimeout() is the key here. It puts console.log(2) into message queue, whereas console.log(3) is put into stack and excuted immediately, even if I set second argument as 0. After stack is empty, event loop checks tasks in queue, and finds console.log(2) , and then puts it to stack to be processed.

A practice given JavaScript runtime modle is that do not try to make one function takes too long.

After ES6, there are three ways to implement asynchronous programming in JavaScript: Callback, Promise, and Async/await

Callback

A callback is a function passed into another function as an argument by which it is then invoked inside the outer function to complete some kind of routine or action. A callback fuction can be called in either sync or async ways. Here is an example to tell the difference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function barSync(val, callback) {
callback(val);
}

function barAsycnc(val, callback) {
setTimeout(() => {
callback(val);
}, 0);
}

function myCallback(val) {
console.log(val);
}

console.log(1);
barSync(2, myCallback);
console.log(3);

console.log("---");
console.log(1);
barAsycnc(2, myCallback);
console.log(3);

Output:

1
2
3
4
5
6
7
1
2
3
---
1
3
2

However, in asynchronous context, the callback should be called whenever the outer function’s asynchronous process is done. Usually, the result from outer function is passed to the callback. One example from fs.readFile (I created a file named a.txt with content “Hello World”):

1
2
3
4
5
6
7
8
9
10
var fs = require("fs");

var content;

fs.readFile("./a.txt", "utf-8", (err, val) => {
console.log("val: ", val);
content = val;
});

console.log("content: ", content);

Output:

1
2
content: undefined
val: Hello World

The callback passed to fs.readFile prints the content of file and assign it to content, but given the output variable content is not assigned. That is asynchronous: the callback is not processed immediately while other functions are processed before it. Only after fs.readFile gets results, and callback becomes the first element in message queue, the callback can be processed.

Promise

Callback Hell is a “well-known” style in JavaScript. It is common to nest 3 or more callbacks given each callback rely on previous callback, which makes the code unreadable, especially when comming into error handling. Then people come up with Promise that became standard in ES6. There are numerous tutorials about Promise, here just give a single example of how Promise performs asynchronous operation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const square = new Promise((resolve) => {
resolve(1);
});

square
.then((val) => Promise.resolve(val * 2))
.then((val) => Promise.resolve(val * 2))
.then((val) => console.log("async...", val))
.catch((err) => {
console.log("Error: " + err);
})
.finally(() => {
console.log("promise done");
});

console.log("sync...", square);

Output:

1
2
3
sync... Promise { 1 }
async... 4
promise done

This is a simple example to calculate 2*2. Compared to multi-leveled callbacks, it is more elegant. You can do infinite asynchronous operatios in .then() by chaining forever. And you no longer to nest error handling in callback, while you can just use .catch() to handle the first error encountered in .then() chain.

Async/await

Then what is a more elegant way without .then() chain because it might be still awkward looking? Here comes async/await . Still there are many tutorials so that here just giving an example of how to handle asynchronous operation through async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const num = function (val) {
return new Promise((resolve) => {
resolve(val);
});
};

// 2 * val
async function double(val) {
try {
let res = await num(val);
return res * 2;
} catch (error) {
throw error;
}
}

// sequential execution
async function seq() {
try {
let a = await num(1);
let b = await num(2);
let c = await num(3);
console.log("seq: ", a, b, c);
} catch (error) {
throw error;
}
}

// concurrent execution
async function concur() {
try {
let result = await Promise.all([num(1), num(2), num(3)]);
console.log("concur: ", result);
} catch (error) {
throw error;
}
}

(async function () {
let result = await double(5);
console.log(`2 * 5 = `, result);
})();

seq();
concur();

Output:

1
2
3
2 * 5 = 10
concur: [ 1, 2, 3 ]
seq: 1 2 3

Above is a simple example of async/await usage. In brief, async function returns a Promise; await blocks execution until the promise is resolved or rejected by which it mimics synchronouse operation, and returns the resolved value, while error is catched through try...catch....

In addition, it is elegant to perform concurrency by Promise.all(). You see concur() is printed before seq(). The reason is, although seq() is called firstly, it needs to resolve 3 promises one by one, while concur() resolve the 3 promises at once(not an accurate description but at lease looks like). Thus concur() returns before seq().

Afterword

Promise and async/awiat do give us an easier way to write asychronous codes. However, it trades detailed control for elegancy. In callback you can deal with whatever you want, whereas Promise and async/awiat reduce many details. It might be ok when project is not too complicated, but it does have drawbacks. This is a large topic and will be discussed in future post.

Reference

The Event Loop
Event loop: microtasks and macrotasks
Is JavaScript guaranteed to be single-threaded?
What the heck event loop is (video)