DEV Community

Maciej Wakuła
Maciej Wakuła

Posted on

Asynchronous processing in JavaScript

This is a continuation of Introduction to node and npm

1. Introduction

JavaScript engines are generally single-threaded. Long call processing would normally block any other call but we leverage asynchronous processing to interrupt currently executed procedure allowing engine to switch between the jobs. Behind the scenes Input/Output is multi-threaded.
There are many ways JS developers could achieve effects similar to multi-threading.

2. How asynchronous processing works

You send a message (ex. a request to process a resource like "add a new user to the system") and usually get a confirmation that the request was received. In REST this might be 202 (Accepted) and "location" header, in messaging systems a valid ID of the request.
But the message is only queued for processing, not yet processed.

Whenever system has free resources (ex. is not doing anything more important or queued before your request) then your request could be processed. This takes usually between milliseconds and weeks. It could be for example a request to add a new bank account that must be verified by someone.

When requesting something, often a feedback (confirmation and/or return data) is expected. This is usually done using message sent by the processing system to a place you can access. Often it would contain "correlation-id" that is the ID of the request to indicate it is a response to previous request. Simply saying: We cannot send response (not yet) but we can send instruction (ex. URL) to check if it was already finished.

3. Several attempts to use asynchronous processing

3.1. How the code execution gets postponed

In JavaScript calls could be postponed whenever you use a promise (or async/await which is using promises behind the scene).

async function log1(msg) {
  await console.log(msg);
}
async function log2(msg) {
  console.log(msg);
}
function log3(msg) {
  console.log(msg);
}
function log4(msg) {
  return new Promise(resolve=>console.log(msg));
}

setTimeout(()=>log1(1), 0);
setTimeout(()=>log2(2), 0);
setTimeout(()=>log3(3), 0);
setTimeout(()=>log4(4), 0);
Enter fullscreen mode Exit fullscreen mode

Usually prints 1,2,3,4 as expected.
Internally those are 4 different calls scheduled to happen with 0ms delay (no delay). And the order of execution doesn't need to keep the sequence.

log1 is async (returns a promise) and stops execution when calling console.log because there is await. Once called, the promise is not yet fulfilled - it would be after console.log returned,then log1 could continue, and finally it returns. You are sure that console.log returns before log1 gets fulfilled (only because there is "await").

log2 is async (returns a promise) and triggers console.log and returns. You have no guarantee that console.log prints output before log2 gets fulfilled.

log3 is just a function. The console.log might be scheduled but log3 returns and continues to execute your code. The JavaScript thread is blocked until it returns (though it can schedule some promises).

log4 returns a promise. Its execution could be delayed (then you see that returned promise is "pending" until it gets "fulfilled").

Think about it thoroughly and you could also omit setTimeout in 3 out of 4 cases - then call is made and a "pending promise" is returned. In fact the case of log3 is probably most dangerous.

If you are using node, then internally libuv is used for calls and also for anything like input/output (ex. to print something onto the console).

3.2. Calling other applications

You can sent a request to another system, service or application. You can send a REST request and should wait for the response (even "Accepted" when task is only queued). Or publish this to a queue yourself.

3.3. Workers

Node can run parallel instances of node called "workers". Workers can act as a parallel process to perform heavy work without blocking the main thread.

4. What to look at

4.1. Intervals

You can schedule a method to be called with a fixed interval:

let counter = 0;
let intervalHandle = undefined;
const intervalInMilliseconds = 10;

function myMethod() {
  counter++;
  for(let i = 0; i < 9999; i++){console.log(i);}

  if(counter > 10) {
    clearInterval(intervalHandle);
    intervalHandle = null;
  }
}

intervalHandle = setInterval(myMethod, intervalInMilliseconds);
Enter fullscreen mode Exit fullscreen mode

You could expect that it prints number from 0 to 9999 every 10ms until 10 iterations are made within 100ms.
The issue is that you have no guarantee that methods would be called every 10ms. This could lead to 2 problems:

  • Your method could be called more than once at the same time (if it is using asynchronous processing internally)
  • Your method could be executed with longer interval (it needs to print the numbers first)

A bit corrected code:

let counter = 0;
const minimumIntervalInMilliseconds = 10;
function myMethod() {
  counter++;
  for(let i = 0; i < 9999; i++){console.log(i);}

  if(counter <= 10) {
    setTimeout(myMethod, minimumIntervalInMilliseconds);
  }
}

setTimeout(myMethod, minimumIntervalInMilliseconds);
Enter fullscreen mode Exit fullscreen mode

4. Dangers of single-threaded app

If your application runs inside a docker container that has a healthcheck and restarts whenever healthcheck is not responded (ex. timeout 1s, interval 1s), then any job blocking main thread for 1000ms would result in container being restarted.

If your app runs on kubernetes and your is blocked over the timeout threshold then your application might be "disconnected" (readiness probe) or restarted (liveness probe).

If you are connected to a queue (ex. kafka or RabbitMQ) and your application is busy for the timeout (ex. 3000ms for kafka subscriber) then your application will be kicked-out and considered "dead". I have seen this scenario where developers tried to update RabbitMQ and its libraries searchign for an issue in the server - where the cause was in the node code.

Top comments (0)