Sitemap

JavaScript

9 min readSep 6, 2026

--

Press enter or click to view image in full size

The fundamental concepts we will cover

In this blog, we will explore some of the fundamental concepts that help us understand how JavaScript works:

  1. Event Loop — Call Stack, Heap, Web APIs, Promises, and Microtasks/Macrotasks
  2. Stack Frames
  3. DOM
  4. Closures and Garbage Collection
  5. Promises, Callbacks, and Async/Await
  6. Generator Functions

Event Loop

The Event Loop is one of the mechanisms that allows JavaScript to handle asynchronous operations without blocking the main thread.

To understand it, we need to look at several parts of the JavaScript runtime:

  • Call Stack
  • Heap
  • Web APIs in the browser, or Node.js APIs in Node.js
  • Callback queues
  • Microtask queues
  • Event Loop

The important idea is that JavaScript executes code on a Call Stack, while asynchronous operations can be handled by the surrounding runtime. When those operations finish, their callbacks can be scheduled for execution.

How the Event Loop works

The following diagram represents the main idea:

┌───────────────────┐
│ CALL STACK │
│ JavaScript │
└─────────┬─────────┘
│
│ Synchronous code finishes
▼
┌───────────────────┐
│ MICROTASK QUEUE │
│ │
│ Promise.then() │
│ Promise.catch() │
│ queueMicrotask() │
└─────────┬─────────┘
│
▼
╔═════════════════════════════════════════════╗
║ EVENT LOOP ║
║ ║
║ ┌───────────────┐ ║
║ │ TIMERS │ ║
║ └───────┬───────┘ ║
║ ▼ ║
║ ┌───────────────┐ ║
║ │ PENDING │ ║
║ │ CALLBACKS │ ║
║ └───────┬───────┘ ║
║ ▼ ║
║ ┌───────────────┐ ║
║ │ POLL │ ║
║ │ I/O │ ║
║ └───────┬───────┘ ║
║ ▼ ║
║ ┌───────────────┐ ║
║ │ CHECK │ ║
║ │ setImmediate │ ║
║ └───────┬───────┘ ║
║ ▼ ║
║ ┌───────────────┐ ║
║ │ CLOSE │ ║
║ │ CALLBACKS │ ║
║ └───────┬───────┘ ║
║ └───────────────────────────────┐ ║
║ │ ║
╚══════════════════════════════════════════╪══╝
│
▼
┌───────────────────┐
│ CALL STACK │
│ Execute JS │
└───────────────────┘

Important: This diagram describes the Node.js Event Loop. Browsers have a similar general mechanism, but their APIs and event-loop details are different.

The Call Stack

The Call Stack is where JavaScript keeps track of the functions that are currently executing.

It follows the LIFO principle:

Last In, First Out

For example:

function func1() {
func2();
}
function func2() {
func3();
}
function func3() {
console.log("Hello");
}
func1();

The execution order is:

func1()
↓
func2()
↓
func3()
↓
console.log()

The Call Stack works like a stack of plates. The last function added to the stack is the first one to finish.

The Heap

The Heap is a region of memory used to store dynamically allocated data, such as objects, arrays, and other values that need memory beyond the simple execution state of a function.

For example:

const user = {
name: "Rayane",
age: 20
};

The object is stored in memory managed by the JavaScript runtime.

The Call Stack and Heap are different, but they work together:

CALL STACK                  HEAP
┌───────────────┐ ┌────────────────────┐
│ user variable │─────────▶│ { │
│ │ │ name: "Rayane", │
└───────────────┘ │ age: 20 │
│ } │
└────────────────────┘

The variable user holds a reference to the object in the Heap.

What does the Event Loop do?

The Event Loop coordinates the execution of JavaScript and the processing of asynchronous callbacks.

When an asynchronous operation finishes, its callback becomes eligible to run. The Event Loop helps determine when that callback can be moved to the Call Stack.

The general process is:

JavaScript code
│
▼
V8 Engine
│
▼
Call Stack
│
▼
Node.js APIs
│
▼
libuv
│
├───────────────┐
▼ ▼
OS Thread Pool
│ │
└───────┬───────┘
▼
Asynchronous operation
│
▼
Callback becomes ready
│
▼
Event Loop
│
▼
Callback Queue
│
▼
Call Stack

Note: This is a simplified diagram. The exact path depends on the operation. For example, network I/O and file-system operations do not always use the same mechanism.

Stack Frames

A Stack Frame is the information created on the Call Stack for one function call.

It contains the information needed while the function is executing, such as:

  • Parameters
  • Local variables
  • The return address
  • Other execution information

When the function finishes, its stack frame is removed from the Call Stack.

Example

function add(a, b) {
const result = a + b;
return result;
}
add(2, 3);

When add(2, 3) runs, a stack frame is created:

     CALL STACK
┌─────────────────────┐
│ stack frame: add │
│ │
│ a = 2 │
│ b = 3 │
│ result = 5 │
│ return → caller │
└─────────────────────┘

When the function finishes:

     CALL STACK
┌─────────────────────┐
│ frame removed │
└─────────────────────┘

In short

Function call
↓
Stack Frame created
↓
Function executes
↓
Function returns
↓
Stack Frame removed

DOM

The DOM (Document Object Model) is the browser’s representation of an HTML document.

When the browser loads an HTML page, it creates a tree of objects called nodes.

JavaScript can use the DOM to:

  • Read HTML elements
  • Modify their content
  • Add new elements
  • Remove elements
  • Change styles and attributes

The DOM is provided by the browser, not by JavaScript itself.

Example

HTML:

<h1 id="title">Hello</h1>

JavaScript:

const title = document.getElementById("title");
title.textContent = "Hello Rayane!";

The browser represents the HTML element as an object:

document
│
▼
<h1 id="title">
│
▼
"Hello"

After executing:

title.textContent = "Hello Rayane!";

The DOM becomes:

document
│
▼
<h1 id="title">
│
▼
"Hello Rayane!"

The browser can then update what is displayed on the page.

Callbacks

A callback is a function that we pass to another function so that it can be called later.

Example

function greet(name) {
console.log(`Hello ${name}`);
}
function execute(callback) {
callback("Ajami");
}
execute(greet);

Output:

Hello Ajami

Here, greet is passed as an argument to execute.

The execute function receives it as callback and calls it:

callback("Ajami");

A callback does not have to be asynchronous. It can also be called immediately, as in this example.

Promises

A Promise is a JavaScript object that represents the eventual result of an asynchronous operation.

For example, when we request data from a database or another service, we usually do not want our JavaScript execution to stop while waiting for the result.

Instead, we start the operation and let JavaScript continue executing other code. When the operation finishes, the Promise settles, and we can handle its result.

This is where the Promise object and its callbacks become useful.

Example

const promise = fetch("https://api.example.com/data");

Here, fetch() returns a Promise.

We can attach callbacks to it using:

  • .then()
  • .catch()
  • .finally()
fetch("https://api.example.com/data")
.then((response) => {
console.log("Request finished");
})
.catch((error) => {
console.log("Something went wrong:", error.message);
})
.finally(() => {
console.log("Request completed");
});

The important idea is:

We start an asynchronous operation, and JavaScript can continue executing other code while waiting for its result.

Promise states

A Promise has three possible states:

PENDING
│
├───────────────┐
▼ ▼
FULFILLED REJECTED
  • Pending: The operation is still in progress.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

A Promise can settle only once.

Async/Await

An async function always returns a Promise.

Get rayane Lamlaika’s stories in your inbox

Join Medium for free to get updates from this writer.

The await keyword allows us to write asynchronous code in a style that looks more like synchronous code.

Example

async function leetfinish() {
console.log("The function starts");
const res = await fetch("https://api.example.com/finish");
console.log("The request finished");
}
leetfinish();
console.log("Let's review our code");

Possible output:

The function starts
Let's review our code
The request finished

Why does this happen?

When JavaScript reaches:

const res = await fetch("https://api.example.com/finish");

the fetch() operation starts.

The await pauses the execution of this async function until the Promise settles.

It does not freeze the JavaScript thread.

While the function is waiting, JavaScript can execute other work.

When the Promise settles, the function can resume.

Important distinction

await pauses the function, not the thread.

This is why we can see:

The function starts
Let's review our code
The request finished

The function is waiting for the network request, but the rest of the JavaScript execution can continue.

Handling Errors with Async/Await

With .then() and .catch(), we handle errors using Promise methods.

With async/await, we can use try/catch/finally.

Example

let isLoading = true;
async function checkBudget() {
try {
// fetch() returns a Promise.
// await pauses this function until the Promise settles.
const response = await fetch(
"https://api.example.com/khouribga/business/budget"
);
// fetch() does NOT reject automatically for HTTP errors
// such as 404 or 500.
if (!response.ok) {
throw new Error("Failed to fetch business data");
}
// response.json() also returns a Promise.
const business = await response.json();
if (business.budget < 10) {
throw new Error("Budget is too low");
}
console.log("Budget is sufficient:", business.budget);
} catch (error) {
console.log("Something went wrong:", error.message);
} finally {
// This runs whether the request succeeds or fails.
isLoading = false;
console.log("Loading finished:", isLoading);
}
}
checkBudget();

What happens here?

  1. The function starts.
  2. fetch() starts the request.
  3. await pauses the function.
  4. When the response arrives, the function resumes.
  5. We check whether the HTTP request was successful.
  6. We parse the JSON response.
  7. We check the budget.
  8. If an error occurs, catch handles it.
  9. finally runs whether the request succeeds or fails.

Execute Asynchronous Functions in Parallel

A useful LeetCode problem for practicing asynchronous JavaScript is:

2721. Execute Asynchronous Functions in Parallel

This problem helps us understand how multiple asynchronous operations can be started without waiting for each one to finish before starting the next.

JavaScript Generators

A generator function is a special type of function that can pause its execution and resume it later.

This allows it to produce multiple values over time, instead of returning everything at once.

You define a generator function using function*:

function* generatorAjami() {
// Instructions ...
yield i;
// More instructions ...
yield i;
// More instructions ...
}

The keyword is yield.

You can think of yield as a special kind of return:

  • return gives back a value and finishes the function.
  • yield gives back a value but pauses the function, saving its current execution state.

Example

function* generatorAjami() {
console.log("First born in casa");
yield 10;
console.log("Second move to Khouribga");
yield 20;
console.log("Third live in Khouribga");
yield 30;
}

Calling the generator

const generator = generatorAjami();

The function does not execute its instructions yet.

Instead, it returns a generator object.

This object is also an iterator, meaning it allows us to move through the values produced by the generator.

We can use .next() to ask the generator for its next value:

console.log(generator.next());

Output:

First born in casa
{ value: 10, done: false }

The generator executes until it reaches:

yield 10;

It pauses there and gives us 10.

The returned object has two important properties:

  • value → the value produced by the generator.
  • done → a boolean indicating whether the generator has finished.

So:

{ value: 10, done: false }

means:

“Here is the value 10. I have not finished yet.”

Calling .next() again

console.log(generator.next());

Output:

Second move to Khouribga
{ value: 20, done: false }

Notice that "Second move to Khouribga" is printed now, not during the first call.

That is because the generator resumes exactly where it stopped, after the previous yield.

Then:

console.log(generator.next());

Output:

Third live in Khouribga
{ value: 30, done: false }

And finally:

console.log(generator.next());

Output:

{ value: undefined, done: true }

This means:

“There are no more values to produce. The generator has finished.”

The important idea

A generator is not a function that returns multiple times.

A function still finishes only once.

The difference is that a generator can pause and resume, allowing us to obtain several values from the same function execution.

Visualizing the generator

generator.next()
↓
Execute instructions
↓
yield 10
↓
Pause and return { value: 10, done: false }
generator.next()
↓
Resume after yield 10
↓
yield 20
↓
Pause and return { value: 20, done: false }
generator.next()
↓
Resume after yield 20
↓
yield 30
↓
Pause and return { value: 30, done: false }
generator.next()
↓
No more instructions
↓
Return { value: undefined, done: true }

Generate Fibonacci Sequence

Another useful LeetCode problem for practicing generators is:

2648. Generate Fibonacci Sequence

The idea is to use a generator to produce Fibonacci numbers one at a time.

var fibGenerator = function* () {
let a = 0;
let b = 1;
while (true) {
yield a;
const next = a + b;
a = b;
b = next;
}
};

How it works

a = 0, b = 1
↓
yield 0
↓
a = 1, b = 1
↓
yield 1
↓
a = 1, b = 2
↓
yield 1
↓
a = 2, b = 3
↓
yield 2

The generator can continue producing values indefinitely, but it only calculates the next value when .next() is called.

Conclusion

┌─────────────────────────────────────────────┐
│ NODE.JS │
│ │
│ ┌─────────────┐ │
│ │ V8 │ │
│ │ │ │
│ │ Call Stack │ │
│ │ Heap │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Node.js APIs │ │
│ └───────┬───────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ libuv │ │
│ │ │ │
│ │ ┌─────────┐ │ │
│ │ │ Event │ │ │
│ │ │ Loop │ │ │
│ │ └─────────┘ │ │
│ │ │ │
│ │ Thread Pool │ │
│ └───────┬───────┘ │
│ │ │
└───────────┼─────────────────────────────────┘
│
▼
┌───────────────┐
│ OS │
│ │
│ Networking │
│ Kernel │
│ epoll/kqueue │
└───────────────┘

The main idea is that JavaScript executes code through the Call Stack, while the runtime provides mechanisms for handling asynchronous operations.

The Event Loop helps coordinate when callbacks can run.

The Heap stores dynamically allocated data.

Promises represent future results.

Async/await makes asynchronous code easier to read.

Generators allow functions to pause and resume.

Understanding these concepts gives us a much stronger foundation for working with Node.js, backend development, and asynchronous JavaScript.

--

--