⚡ JavaScript

Multihilo y asincronía en JavaScript (Node.js): guía detallada

What is it and why? (a real-life analogy)

Imagine you are a head chef in a restaurant kitchen. You need to prepare three dishes: soup, salad, and dessert. You only have one stove (that's our processor). How will you cook?

Sequential approach (synchronous code): You put the soup on to boil and stand next to it until it boils. Then you start cutting vegetables for the salad, without getting distracted. Then you bake the dessert. Everything is done strictly one after the other. If the soup takes 30 minutes to cook, you just wait for 30 minutes, doing nothing. This is very inefficient!

Multithreading (like in C++ or Java): You hire two assistant chefs. Now you have three people in the kitchen. One boils the soup, the second cuts the salad, the third handles the dessert. Everyone works simultaneously. But! You need to make sure no one interferes with each other (doesn't take the same pot, doesn't use the same knife). This is difficult to manage.

Asynchrony (like in JavaScript): You remain alone, but change your approach. You put the soup on the stove, set a timer, and, while the soup is cooking, go cut the salad. The timer "beeps" — you return, turn off the soup, set it aside to cool, and continue with the salad. Then you put the dessert in the oven and, while it bakes, you chop the salad. You don't wait — you switch between tasks when one of them requires waiting (boiling, baking).

JavaScript (Node.js) uses exactly this third approach. It is called asynchronous programming using the Event Loop. This allows one person (one thread) to do a great many things without idling while waiting.

Important: Node.js does not have true multithreading in the classical sense (like in Python or Java). There is one main thread that manages everything. But asynchrony creates the illusion of multitasking, and this is enough for 99% of web applications.

Theory: how it works internally

To understand asynchrony, you need to understand four key concepts: Call Stack, Web APIs (or libuv in Node.js), Callback Queue, and Event Loop.

1. Call Stack

This is a list of what is currently being executed. When you call a function, it is placed on the stack. When a function completes, it is removed from the stack. JavaScript only executes what is at the top of the stack. This is a strict order: one command at a time.

2. Web APIs / libuv

When you call an asynchronous function (e.g., setTimeout, fetch, reading a file), it is not executed in JavaScript. It is passed to a special browser environment (or to the libuv library in Node.js). This environment handles timers, network requests, disk reads. JavaScript does not wait — it moves on.

3. Callback Queue

When an asynchronous operation completes (timer fires, file is read), its callback function goes into a queue. This is like a "waiting list". The functions stand in line and wait for the call stack to become empty.

4. Event Loop

This is an infinite loop that does one simple thing: checks if the call stack is empty. If the stack is empty, it takes the first function from the callback queue and places it on the stack for execution. And so on infinitely.

// Visualization of the Event Loop's work
console.log('1. Start');

setTimeout(() => {
  console.log('3. Timer fired');
}, 0); // Even with a 0 ms delay

console.log('2. End');

// Console output:
// 1. Start
// 2. End
// 3. Timer fired

Why is that? Because setTimeout is passed to libuv, and JavaScript moves on, executing console.log('2. End'). Only when the stack is empty does the Event Loop take the callback from the queue and execute it.

Syntax in JavaScript (Node.js): all forms and variants

There are three main ways to work with asynchrony in JavaScript. They evolved from simple to complex.

1. Callbacks — the old way

A function that is passed to another function and called after the operation completes. This was the first and simplest way, but it leads to "callback hell" — when nesting becomes too deep.

const fs = require('fs');

// Reading a file using a callback
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Read error:', err);
    return;
  }
  console.log('File contents:', data);
});

2. Promises — the modern standard

A Promise is an object that represents the result of an asynchronous operation. It can be in three states:

  • Pending — the operation has not completed yet
  • Fulfilled — the operation completed successfully
  • Rejected — an error occurred
// Creating a promise
const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Success! Data received');
    } else {
      reject('Error! Something went wrong');
    }
  }, 1000);
});

// Using the promise
myPromise
  .then(result => {
    console.log(result); // 'Success! Data received'
  })
  .catch(error => {
    console.error(error); // 'Error! Something went wrong'
  });

3. Async/Await — sugar over promises

This is syntactic sugar that makes asynchronous code look like synchronous code. The keywords are async and await.

// A function marked with async always returns a Promise
async function getData() {
  try {
    // await pauses the execution