Online Compiler logoOnline Compiler

JavaScript Tutorial

How JavaScript Works — Engine, Call Stack, and Event Loop

JavaScript runs on a single thread but handles async tasks with the event loop.

Understanding execution context and queues helps you debug and write reliable code.

Why We Need It

These internals explain async behavior, hoisting, and interview questions.

Better understanding leads to cleaner, faster, and safer JavaScript.

Syntax

callStack.push(task);
webAPIs.run(asyncTask);
taskQueue.enqueue(callback);
eventLoop.tick();

Basic Example

1. Single-Threaded Nature

console.log("Task 1");
console.log("Task 2");

setTimeout(() => console.log("Async"), 0);

JavaScript runs one instruction at a time; async work is queued.

Real World Example

2. Hoisting Example

console.log(name); // undefined
var name = "Alice";

function greet() {
  console.log("Hello " + name);
}
greet();

var declarations and function declarations are hoisted.

Multiple Use Cases

JavaScript Is Single-Threaded (But Very Clever)

JavaScript has a single-threaded execution model, meaning one instruction executes at a time on a single thread.

Despite this limitation, JavaScript handles complex, multi-scenario applications through the event loop and asynchronous patterns.

Understanding this is crucial because it explains why setTimeout doesn't guarantee immediate execution and why async/await works the way it does.

The single-threaded model actually provides advantages: simpler reasoning about code, no race conditions at the language level, and easier debugging.

The JavaScript Engine

A JavaScript engine is a program that reads, compiles, and executes JavaScript code.

Different browsers use different engines optimized for performance and features.

Modern engines use Just-In-Time (JIT) compilation, which translates frequently-used code to machine code for faster execution.

The engine manages memory allocation, garbage collection, and code optimization.

Understanding the engine helps you write code that performs well and avoids common memory leaks.

Execution Phases: Creation and Execution

JavaScript code execution happens in two distinct phases: Creation Phase and Execution Phase.

During the Creation Phase, the JavaScript engine scans the code and allocates memory for variables and functions before any code actually runs.

This happens silently, and it's why hoisting occurs - variables and functions are allocated before line-by-line execution begins.

Understanding these phases explains JavaScript's quirks like variable hoisting, function hoisting, and the temporal dead zone.

The execution phase is where code actually runs, variables are assigned, and functions are called.

Execution Context and Scope Chain

Every piece of code that runs in JavaScript runs within an Execution Context.

A new execution context is created for the global scope and each function call.

Each execution context has access to three components: Variable Object, Scope Chain, and the 'this' value.

The scope chain determines what variables are accessible from within a function or block.

This is how JavaScript implements nested function behavior and why inner functions can access outer function variables.

The Call Stack in Action

The call stack is a data structure that tracks function calls and execution order.

When a function is called, it's pushed onto the stack. When it returns, it's popped off.

If a function calls another function, the new function is pushed on top, and the original pauses.

The call stack ensures functions execute in the correct order and that local variables are created and destroyed properly.

Stack overflow occurs when the call stack grows too large (usually due to infinite recursion).

The Event Loop and Web APIs

The Event Loop is what makes JavaScript's apparent concurrency possible in a single-threaded environment.

When asynchronous operations like setTimeout, fetch, or DOM events occur, they're delegated to Web APIs (provided by the browser).

The Web APIs run these operations outside the main thread and queue the callback functions.

The Event Loop constantly checks if the call stack is empty. When it is, it moves callbacks from the queue to the call stack.

This allows JavaScript to handle I/O and other time-consuming operations without blocking user interactions.

Microtasks vs Macrotasks

Not all callbacks are equal. JavaScript has two types of task queues: Microtask Queue and Macrotask (Task) Queue.

Microtasks include Promise callbacks, queueMicrotask, and MutationObserver callbacks.

Macrotasks include setTimeout, setInterval, setImmediate, I/O, and UI rendering.

After each macrotask, ALL microtasks are processed before the next macrotask runs.

This priority system is crucial for understanding async code behavior and interview questions.

Memory Management and Garbage Collection

JavaScript manages memory automatically through garbage collection.

Memory is allocated when values are created and deallocated when values are no longer needed.

The call stack stores primitive values and references, while the heap stores complex objects.

Garbage collectors determine when objects are no longer reachable and free their memory.

Understanding memory helps you avoid memory leaks, especially with event listeners and closures.

More Examples

3. Call Stack Visualization

function a() { b(); }
function b() { c(); }
function c() { console.log("c"); }

a();

Functions are pushed and popped from the call stack in order.

4. Event Loop with setTimeout

console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
console.log("End");

Callbacks run after the call stack clears.

5. Microtasks vs Macrotasks

console.log("Script");
Promise.resolve().then(() => console.log("Promise"));
setTimeout(() => console.log("Timeout"), 0);

Microtasks (Promises) run before macrotasks (setTimeout).

Comparison

Without

// Sequential, blocking mindset
fetchData();
processData();

With

// Event loop mindset
fetchData().then(processData);

Common Mistakes and Fixes

Thinking async code runs in parallel threads

JavaScript is single-threaded. Async operations are delegated to Web APIs and queued for later execution.

Assuming setTimeout(fn, 0) runs immediately after

It queues the callback. It only runs when the call stack is clear AND the event loop processes it.

Not understanding hoisting

Variables and functions are allocated memory during creation phase, before execution phase begins.

Confusing microtasks and macrotasks

Microtasks (Promises) always run before macrotasks (setTimeout) in the same event loop cycle.

Ignoring scope chain behavior

Inner functions access outer scope through the scope chain, but outer functions cannot access inner scope.

Interview Questions

Is JavaScript single-threaded?

Yes. It runs on one main thread and uses the event loop for async work.

Why do promises run before setTimeout?

Promises are microtasks and are prioritized over macrotasks.

What causes stack overflow?

Deep or infinite recursion filling the call stack.

Practice Problem

Practice: Predict the output order of logs with setTimeout and Promise.

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");

One Possible Solution

// Output:
// A
// D
// C
// B

Frequently Asked Questions

Is JavaScript single-threaded?

Yes, JavaScript runs on a single main thread. However, Web APIs handle time-consuming operations, and the event loop coordinates callbacks.

How does async/await work internally?

Async functions are syntactic sugar over Promises. They pause execution at await points and resume when the Promise settles.

Why do Promises run before setTimeout?

Promise callbacks are microtasks with higher priority than macrotasks like setTimeout. The event loop processes all microtasks before the next macrotask.

What causes stack overflow?

Infinite recursion or excessively deep call stacks. Each recursive call adds to the stack until memory is exhausted.

Is hoisting good or bad?

Hoisting is how JavaScript works, not good or bad. Using const/let and function declarations helps you work with it effectively.

Can I access closure variables?

No, closure variables are private by design. This is what makes closures useful for data encapsulation.

When should I use microtasks vs macrotasks?

Rarely. Use Promises for operations that should complete first, setTimeout for lower-priority deferred work.

Do I need to optimize memory manually?

Rarely. The garbage collector handles most cleanup. Just avoid common patterns like circular references and unused event listeners.

Try It Yourself

Run the setTimeout example and observe the log order.