JavaScript is Single-Threaded: Understanding Hoisting in Sequential Execution

Introduction: The Paradox

Here's a question that confuses many developers: If JavaScript is single-threaded, how can hoisting work? Shouldn't statements execute sequentially from top to bottom?

// This actually works!
console.log(message); // Output: undefined (NOT an error)

var message = "Hello!";

This article explains the paradox: How JavaScript can be single-threaded yet not strictly sequential.

What Does "Single-Threaded" Actually Mean?

Single-threaded means JavaScript can only execute one piece of code at a time. It cannot run multiple tasks in parallel.

Single-Threaded Languages

  • JavaScript (in browsers and Node.js)
  • Python (with the GIL - Global Interpreter Lock)
  • Ruby

Multi-Threaded Languages

  • Java
  • C++
  • Go
  • Rust

Being single-threaded doesn't mean sequential! This is the key insight.

Sequential vs Single-Threaded: What's the Difference?

CharacteristicSequential ExecutionSingle-Threaded (JavaScript)
Execution OrderTop-to-bottom, line by lineDetermined by execution phases + event loop
Pre-processing PhaseNoneHoisting (creation phase before execution)
Concurrent TasksStrictly one at a timeOne at a time (but with async patterns)
Code ExampleAlways executes in source code orderDeclarations executed in creation phase

The Two Phases of JavaScript Execution: The Secret

JavaScript execution happens in two distinct phases:

Phase 1: Creation (Compilation) Phase

Before any code executes, JavaScript's engine:

  • Scans the entire code
  • Creates variables and function declarations in memory
  • Sets up the execution context
  • Does NOT execute any code yet

Phase 2: Execution Phase

After creation, the engine:

  • Executes statements line by line
  • Assigns values to variables
  • Calls functions

Visual Representation of the Two Phases

Your Code: console.log(getName()); // Output: "John" var age = 25; function getName() { return "John"; } PHASE 1 (Creation): ├─ Hoist: function getName() {...} ├─ Hoist: var age (initialized as undefined) └─ Setup execution context PHASE 2 (Execution): ├─ Line 1: console.log(getName()) → calls function → "John" ├─ Line 2: age = 25 ├─ Line 3: (function already exists, skip) └─ Done

Let's Walk Through Examples

Example 1: Function Declaration Hoisting

// What you write:
console.log(greet()); // "Hello!"

function greet() {
  return "Hello!";
}

// PHASE 1 (Creation):
// function greet() { return "Hello!"; } → Hoisted completely

// PHASE 2 (Execution):
// console.log(greet()); → Calls function → "Hello!"

Example 2: var Hoisting (The Confusing Part)

// What you write:
console.log(message); // undefined (not an error!)
var message = "Hello!";
console.log(message); // "Hello!"

// PHASE 1 (Creation):
// var message = undefined; → Hoisted and initialized as undefined

// PHASE 2 (Execution):
// console.log(message); → undefined
// message = "Hello!"; → Assignment
// console.log(message); → "Hello!"

Example 3: const/let Hoisting (Temporal Dead Zone)

// What you write:
console.log(name); // ❌ ReferenceError
const name = "Alice";

// PHASE 1 (Creation):
// const name = <uninitialized>; → Hoisted but NOT initialized (TDZ)

// PHASE 2 (Execution):
// console.log(name); → ❌ Error: Still in TDZ!
// const name = "Alice"; → Now initialized, TDZ ends

The Event Loop: How Single-Threaded Execution Feels Asynchronous

The Three Components

1. Call Stack - Where function calls are executed. Only one function at a time.

2. Web APIs / Task Queue - Browser APIs (setTimeout, fetch) that run outside JavaScript. Callbacks go to Task Queue.

3. Event Loop - Checks if call stack is empty, then moves tasks from queue to stack.

Event Loop Example

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");

// Output:
// Start
// End
// Timeout

// Why? Even though setTimeout has 0ms delay:
// 1. "Start" - executed immediately
// 2. setTimeout pushed to Web API (not on call stack)
// 3. "End" - executed immediately
// 4. Call stack empty → event loop moves setTimeout callback
// 5. "Timeout" - executed

Microtask vs Macrotask Queue

console.log("Start");

setTimeout(() => console.log("Timeout"), 0);      // Macrotask

Promise.resolve()
  .then(() => console.log("Promise"));            // Microtask

console.log("End");

// Output:
// Start
// End
// Promise       ← Microtasks run first!
// Timeout       ← Macrotasks run after microtasks empty

Key Interview Question: Explain the Contradiction

📝 Model Answer

The Question: "If JavaScript is single-threaded, why doesn't it execute statements sequentially?"

The Answer: "JavaScript is single-threaded, meaning only one operation executes at a time. However, 'single-threaded' doesn't mean 'strictly sequential.'

JavaScript uses a two-phase execution model:

  1. Creation Phase: The engine scans code and hoists declarations before any execution
  2. Execution Phase: Code runs line by line

Additionally, JavaScript's Event Loop allows asynchronous operations, and it prioritizes microtasks (Promises) over macrotasks (setTimeout)."

Summary: The Two-Phase Model

PhaseWhat HappensExecution Order
CreationEngine scans code, hoists declarations, creates execution contextNOT sequential - all declarations processed first
ExecutionCode runs line by line, values assigned, functions calledSequential within this phase
Event LoopManages async callbacks, microtasks, macrotasksPriority-based (microtasks before macrotasks)

Key Takeaways

🎯 Remember This

  • Single-threaded ≠ Sequential - JavaScript has a sophisticated execution model
  • Two-Phase Execution: Creation phase (hoisting) happens before execution phase
  • Function declarations are fully hoisted - available before their definition
  • var is hoisted and initialized as undefined - so no error, just undefined
  • const/let exist in the Temporal Dead Zone - hoisted but not initialized
  • Event Loop manages asynchronous execution - non-blocking but still single-threaded
  • Microtasks (Promises) run before Macrotasks (setTimeout)

Conclusion

JavaScript is single-threaded, but it's not strictly sequential. The key to understanding this apparent paradox is recognizing that JavaScript execution happens in two phases: the Creation Phase (where hoisting occurs) and the Execution Phase (where statements run sequentially). The Event Loop adds another layer of complexity to execution order.

You Now Understand JavaScript's Execution Model 👆

From hoisting paradoxes to the event loop—you've unlocked knowledge that separates junior from senior developers.

Join Cohort 3 Waitlist →
← Back to Articles