Best Async Document for Typescript

DRAFT: This document is not complete, until you agree the title of this document is accurate.

JavaScript Engine & Runtime Environment

Javascript code runs in a JavaScript engine provided by the runtime environment.

Runtime Environment JavaScript Engine
Node.js V8
Deno V8
Bun JavaScriptCore
Firefox SpiderMonkey
Chrome V8
Edge V8
Safari JavaScriptCore
IE ChakraCore
Oracle Nashorn(deprecated)

In other words, runtime environment (Chrome, Node.js), provides the JavaScript engine (V8). The JavaScript engine provides a single threaded event loop (*) (**).

The JavaScript engine recognizes the functions of the Web/System APIs and hands them off to be handled by the runtime environment. Once those system functions (macro tasks like network, disk, DNS) are finished by the runtime environment, they return and are pushed onto the stack as callbacks. This is managed in the the event loop.

With JavaScript everything is a type of task.

  • console.log() statement is a task
  • const response = await axios.get(yourUrl) is a task
  • setTimeout(function, 100) is a task
  • const file = readFileSync('./filename.txt', 'utf-8'); is a task

Event loop is made of several queues. Tasks, like above, are grouped into different categories and are put into one of the event / task queues.

A special queue is the PromiseJobs queue where the promises created by the developer are processed. This queue is also called “microtask queue”. Practically everything else falls into “macrotasks queue”. Immediately after every macrotask, the engine executes all tasks from microtask queue, prior to running any other macrotasks or rendering (on the browser) or anything else.(*)(**).

Node.js is a runtime environment, doesn’t only run JavaScript but utilize multiple threads(for disk I/O etc). It’s the Node.js event loop which is single threaded. Whenever you do something like IO which is event based, it creates a new thread or utilizes an existing thread from the thread pool for it.

Chrome and Node.js share the same V8 JavaScript engine to run JavaScript but have different Event Loop implementations. The Chrome browser uses libevent as its event loop implementation, and Node.js uses libuv.

Let’s keep these statements in mind.

  • Blocking methods execute synchronously and non-blocking methods execute asynchronously.
  • JavaScript runs in a single thread, but the engine that runs JavaScript is not single threaded.

A Function

Let’s start with a simple function that prints the value provided.

1
2
3
4
5
function log(value: string) {
  console.log(value);
}

log("hello world");

Output

1
 hello world

This is a blocking function and executes synchronously. When it runs nothing else will be executed until it finishes. If it would take 10 minutes to run this function, everything else after this code block would have to wait for 10 minutes.

Let’s print a number of messages.

1
2
3
4
5
6
7
8
9
function log(value: string) {
  console.log(value);
}

log("hello world 1");
log("hello world 2");
log("hello world 3");
log("hello world 4");
log("hello world 5");

Output

1
2
3
4
5
hello world 1
hello world 2
hello world 3
hello world 4
hello world 5

Pretty logical. Each log() function executed sequentially one after another.

Arrow Functions

Before we move forward let’s convert log() function into an arrow function.

1
2
3
4
5
const log = (value: string) => {
  console.log(value);
};

log("hello world");

Output

1
hello world

Same behavior, we just used the arrow notation.

log() is a synchronous (arrow) function.

A Synchronous Function Which Waits

Let’s create a function that takes a while to run.

We have two different cases here:

  • A function that is cpu bound: a function that calculates a big number
  • A function that is waiting on external resources: disk IO, network IO etc.

CPU Bound Function

A CPU bound function, only needs CPU cycles to complete. The freeze() function below, waits for ms milliseconds and blocks the main loop. After ms milliseconds it finishes the execution.

1
2
3
4
const freeze = (ms: number) => {
  const stopTime = Date.now() + ms;
  while (Date.now() < stopTime) {} // Block the main loop
};

Let’s use freeze() function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const freeze = (ms: number) => {
  const stopTime = Date.now() + ms;
  while (Date.now() < stopTime) {}
};

console.log("Start");
const start = Date.now();
freeze(2000);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

1
2
3
Start
Ran for 2000 milliseconds
Finish

Everything above is as expected. JavaScript engine printed Start, then the time it took to run, then Finish.

If we had two tasks each taking 2 seconds, overall the program would run for 4 seconds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const freeze = (ms: number) => {
  const stopTime = Date.now() + ms;
  while (Date.now() < stopTime) {}
};

console.log("Start");
const start = Date.now();
freeze(2000); // first task running for 2 seconds
freeze(2000); // second task running for 2 seconds
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

1
2
3
Start
Ran for 4000 milliseconds
Finish

setTimeout() Method

Executes a specified block of code once after a specified time has elapsed. The timer is provided by the runtime environment, not by the JavaScript Engine. (Example: Node.js implements setTimeout() method, not V8.)

setTimeout(function[, delay, arg1, arg2, …])

function A function to be executed after the timer expires.

delay (Optional) The time, in milliseconds (thousandths of a second), the timer should wait before the specified function or code is executed. If this parameter is omitted, a value of 0 is used, meaning execute “immediately”, or more accurately, the next event cycle. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.

arg1, …, argN (Optional) Additional arguments which are passed through to the function specified by function.

return value The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(); this value can be passed to clearTimeout() to cancel the timeout.

  • NOTE: The specified amount of time (or the delay) is not the guaranteed time to execution, but rather the minimum time to execution. The callbacks you pass to these functions cannot run until the stack on the main thread is empty.

  • As a consequence, code like setTimeout(fn, 0) will execute as soon as the stack is empty, not immediately. If you execute code like setTimeout(fn, 0) but then immediately after run a loop that takes 10 seconds to complete, your callback will be executed after 10 seconds.

We already passed the threshold of lecture to code ratio. Let’s write some code to understand.

An anonymous function that writes hello world after 200 milliseconds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const log = () => {
  console.log("Hello World");
};

console.log("Start");
const start = Date.now();
setTimeout(log, 1000);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

1
2
3
4
Start
Ran for 0 milliseconds
Finish
Hello World

This one is a little strange. Isn’t it? It seems to be out of order.

First we created a function named log() which prints hello world. Later we called setTimeout(log, 1000). This will wait for 1000 milliseconds and then call log() function. Remember, setTimeout() is actually a system call, that delegates the wait operation to Node.js. As soon as we call setTimeout(log, 1000), we push log() to the macrotasks queue we mentioned earlier. For this reason, code ran sequentially, as if it ignored setTimeout(log, 1000);. After 1000 milliseconds, log() function was pushed to the queue as a callback and executed. Finally, it printed Hello World.

Welcome to the async world of JavaScript.

Any function that sends a code block to a queue is an async function. Event loop is the code that checks every queue one after another and runs one or more tasks (code blocks) in them. TODO: improve wording here

An ugly representation is below; (TODO: Improve the presentation)

      |
console.log("Start");
      |
const start = Date.now();
      |
       --------------> push setTimeout(log, 1000); to macrotasks
      |
const finish = Date.now();
      |
console.log(`Ran for ${finish - start} milliseconds`);
      |
console.log("Finish");
      |
1000 milliseconds later
      |
    log();

By the way even if we set timeout value to 0 milliseconds, the code above would follow the exact same order. Let’s see.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const log = () => {
  console.log("Hello World");
};

console.log("Start");
const start = Date.now();
setTimeout(log, 0); // same as setTimeout(log);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

1
2
3
4
Start
Ran for 0 milliseconds
Finish
Hello World

Because setTimeout() is delegated to macrotasks queue, and because only one item from the macrotasks queue is pulled with every iteration of the loop, Hello World is printed at the very end.

The Problem

What if we needed to do something after the async function completes?

Let’s say we have two functions. getMessage() gets a message from the internet and printMessage() prints the message retrieved by getMessage(). It takes time for getMessage() to get the message.

1
2
getMessage();
printMessage();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// async function
const getMessage = () => {
  console.log("getting the message");
  setTimeout(() => {
    console.log("retrieved the message");
  }, 1000);
};

// not an async function
const printMessage = () => {
  console.log("printing message");
};

getMessage();
printMessage();

Output

1
2
3
getting the message
printing the message
retrieved the message // after 1 second later

Not good. We printed the message before we received it.

I have an idea, let’s move printMessage() into getMessage()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const getMessage = () => {
  console.log("getting the message");
  setTimeout(() => {
    console.log("retrieved the message");
  }, 1000);
  printMessage();
};

const printMessage = () => {
  console.log("printing message");
};

getMessage();

Output

1
2
3
getting the message
printing the message
retrieved the message

Yikes. Let’s try again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const getMessage = () => {
  console.log("getting the message");
  setTimeout(() => {
    console.log("retrieved the message");
    printMessage();
  }, 1000);
};

const printMessage = () => {
  console.log("printing message");
};

getMessage();

Output

1
2
3
getting the message
retrieved the message
printing the message

Yay. It worked. It worked but what if we want to do something different with the retrieved text? Do we need to change the getMessage() each time?

Callback Functions

Instead of changing the getMessage() each time, we can pass a function to printMessage().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const getMessage = (callback: Function) => {
  console.log("getting the message");
  setTimeout(() => {
    console.log("retrieved the message");
    // pretending like it take time to finish
    callback();
  }, 1000);
};

const printMessage = () => {
  console.log("printing message");
};

getMessage(printMessage);

Output

1
2
3
getting the message
retrieved the message
printing the message

We are getting there. const getMessage = (callback: Function) => { here we are passing a callback function to getMessage() and calling back that function when we have what we were waiting for. We call the function a callback function because we call it back after we are done in the current function.

A better implementation is below. getMessage(index: number, cb: Function) accepts an index to know which message to get, and a function to call back after the message with the index number is received.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const getMessage = (index: number, cb: Function) => {
  const messages = ["Hello World", "Hello Universe"];
  const message = messages[index % messages.length];

  console.log("LOG: getting the message");
  setTimeout(() => {
    console.log(`LOG: retrieved: "${message}"`);
    cb(message);
  }, 1000);
};

const printMessage = (message: string) => {
  console.log("LOG: printing the message");
  console.log(message);
};

getMessage(0, printMessage);

Output

1
2
3
4
LOG: getting the message
LOG: retrieved: "Hello World"
LOG: printing the message
Hello World

What if we wanted to add a transformation to the text we receive? Let’s convert the text to upper case before we print.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const getMessage = (index: number, cb: Function) => {
  const messages = ["Hello World", "Hello Universe"];
  const message = messages[index % messages.length];

  console.log("LOG: getting the message");
  setTimeout(() => {
    console.log(`LOG: retrieved: "${message}"`);
    cb(message);
  }, 1000);
};

const upperCase = (message: string, cb: Function) => {
  console.log("LOG: converting to upper case");
  const upperCaseText = message.toUpperCase();
  cb(upperCaseText);
};

const printMessage = (message: string) => {
  console.log("LOG: printing the message");
  console.log(message);
};

getMessage(0, (m: string) => upperCase(m, printMessage));

Not so easy to read, especially the very last line. We are calling getMessage() and passing upperCase() function as the callback. Then we are passing printMessage() as a callback to upperCase() function.

What if we wanted to receive messages in order we called them. Let’s modify the code a little bit so it will wait longer for messages[0] than it waits for messages[1].

 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
const getMessage = (index: number, cb: Function) => {
  const messages = ["Hello World", "Hello Universe"];

  const durations = [1000, 500]; // index 0 has 1000 milliseconds

  const message = messages[index % messages.length];
  const duration = durations[index % messages.length];

  console.log(`LOG: getting the message[${index}]`);
  setTimeout(() => {
    console.log(`LOG: retrieved: "${message}"`);
    cb(message);
  }, duration);
};

const upperCase = (message: string, cb: Function) => {
  console.log("LOG: converting to upper case");
  const upperCaseText = message.toUpperCase();
  cb(upperCaseText);
};

const printMessage = (message: string) => {
  console.log("LOG: printing the message");
  console.log(message);
};

getMessage(0, (m: string) => upperCase(m, printMessage));
getMessage(1, (m: string) => upperCase(m, printMessage));

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
LOG: getting the message[0]
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD

We tried to get messages[0], which is Hello World first but instead we got Hello Universe as the first message. We have to change the way we called the getMessage() function if we want to be able to get the messages in the order we called them.

1
2
3
4
getMessage(0, (m: string) => {
  upperCase(m, printMessage);
  getMessage(1, (m: string) => upperCase(m, printMessage));
});

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
LOG: getting the message[0]
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE

What if we had one more message to print? Let’s add another message to messages[] and print all in order.

 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
const getMessage = (index: number, cb: Function) => {
  const messages = ["Hello World", "Hello Universe", "Hello Multiverse"];

  const durations = [1000, 500, 750];

  const message = messages[index % messages.length];
  const duration = durations[index % messages.length];

  console.log(`LOG: getting the message[${index}]`);
  setTimeout(() => {
    console.log(`LOG: retrieved: "${message}"`);
    cb(message);
  }, duration);
};

const upperCase = (message: string, cb: Function) => {
  console.log("LOG: converting to upper case");
  const upperCaseText = message.toUpperCase();
  cb(upperCaseText);
};

const printMessage = (message: string) => {
  console.log("LOG: printing the message");
  console.log(message);
};

const convertAndPrint = (m: string) => {
  upperCase(m, printMessage);
};

getMessage(0, (m: string) => {
  upperCase(m, printMessage);
  getMessage(1, (m: string) => {
    upperCase(m, printMessage);
    getMessage(2, (m: string) => upperCase(m, printMessage));
  });
});

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
LOG: getting the message[0]
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE
LOG: getting the message[2]
LOG: retrieved: "Hello Multiverse"
LOG: converting to upper case
LOG: printing the message
HELLO MULTIVERSE

Imagine we have a chain of these async function;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
asyncFunction(function(){
    asyncFunction(function(){
        asyncFunction(function(){
            asyncFunction(function(){
                asyncFunction(function(){
                    ....
                });
            });
        });
    });
});

This takes us to what is know as callback hell. Imagine adding error handling to the code above. It gets really hard to maintain. There are libraries to manage the chain of callbacks but promises emerged as a better option.

WIP

I decided do publish this post before it is complete. I am going to review the code and add the missing sections in the following days. No promises though.

Promises

TODO: explain promises

Syntax

 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
const upperCase = (message: string): string => {
  console.log("LOG: converting to upper case");
  const upperCaseText = message.toUpperCase();
  return upperCaseText;
};

const printMessage = (message: string) => {
  console.log("LOG: printing the message");
  console.log(message);
};

let getMessage = (index: number): Promise<string> => {
  return new Promise((resolve, reject) => {
    const messages = ["Hello World", "Hello Universe", "Hello Multiverse"];

    const durations = [1000, 500, 750];

    const message = messages[index % messages.length];
    const duration = durations[index % messages.length];

    console.log(`LOG: getting the message[${index}]`);
    setTimeout(() => {
      console.log(`LOG: retrieved: "${message}"`);
      resolve(message);
    }, duration);
  });
};

getMessage(1)
  .then((message) => upperCase(message))
  .then((message) => printMessage(message));

// we could shorten getMessage call as below
// getMessage(1).then(upperCase).then(printMessage);

Async/Await

TODO: explain Async/Await


sources

comments powered by Disqus