In the previous lesson you took something for granted that had not been explained: that the engine "knows" which variables exist in each scope before running a single line. That preparation step is what lets you call a function declaration before writing it, what makes a var be undefined instead of raising an error, and what produces the Cannot access 'x' before initialization you have already seen three times. In this lesson you will take that mechanism apart: the two phases every scope goes through, hoisting, the temporal dead zone, the execution context and the call stack you only glimpsed in 03-01. The practical goal is twofold: to understand any error trace, and to organize Nómada Tasks's js/app.js file so that none of this ever matters.

Contents

  1. The engine does not run on the first pass
  2. The creation phase and the execution phase
  3. Hoisting of function declarations
  4. Hoisting of var
  5. let and const: the temporal dead zone
  6. The complete comparison table
  7. Annotated traces, step by step
  8. The global execution context
  9. Function contexts
  10. The call stack
  11. RangeError: Maximum call stack size exceeded
  12. Reading a real stack trace
  13. Case study: organizing js/app.js
  14. Why modern style makes hoisting irrelevant
  15. Common Mistakes and Tips
  16. Exercises
  17. Conclusion

  1. The engine does not run on the first pass

Intuitively you imagine JavaScript reading the file line by line and running as it goes. That is not what happens. Before running anything in a scope, the engine walks the whole thing to find out which declarations it contains and to reserve room for them.

This code proves it:

console.log(priorityWeight('high'));     // 3   ✓ works
console.log(TODAY);                      // ✗ ReferenceError: Cannot access 'TODAY' before initialization

function priorityWeight(priority) {
  return priority === 'high' ? 3 : 1;
}

const TODAY = '2026-09-20';

Notice the asymmetry: the function can be called earlier; the constant cannot. And —crucially— the error for TODAY is not "is not defined", it is "cannot be accessed yet". The engine knows perfectly well that TODAY exists: it just will not let you use it yet. That only makes sense if there was an earlier pass.

  1. The creation phase and the execution phase

Every time the engine enters a scope (the whole file or a function body), it goes through two phases:

flowchart TD
    A["Entering a scope"] --> B["CREATION PHASE"]
    B --> B1["Register function declarations<br/>complete and ready to use"]
    B --> B2["Register the vars<br/>and initialize them to undefined"]
    B --> B3["Register let/const<br/>UNinitialized → TDZ"]
    B --> C["EXECUTION PHASE"]
    C --> C1["Walk the lines top to bottom"]
    C --> C2["Assign values"]
    C --> C3["Call functions"]

Creation phase (before anything runs):

Kind of declaration What gets registered Initial value
function name() {} The name and the complete function The function, already usable
var x The name undefined
let x / const x The name Nothing: it stays uninitialized
class X {} The name Nothing: TDZ, like let

Execution phase: the lines are walked and the real assignments happen.

The term hoisting describes the observable effect of the creation phase: it is as if the declarations had been moved to the top of the scope. In reality nothing moves; the engine simply already knew about them.

  1. Hoisting of function declarations

Function declarations are hoisted complete: name and body.

console.log(isOverdue('2026-09-05', 'pending', '2026-09-20'));   // true

function isOverdue(dueDate, status, today) {
  return dueDate < today && status !== 'done';
}

This is what 03-01 mentioned in passing: declarations can be called before the line where they are written. This is how the engine "sees" the file:

// --- Creation phase ---
// isOverdue = [complete function]

// --- Execution phase ---
console.log(isOverdue(...));   // it already exists

Function expressions and arrows do not behave this way, because what is hoisted is the variable, not the function:

console.log(typeof declared);          // 'function'
console.log(typeof expressionVar);     // 'undefined'   ← the var exists, but it is undefined
console.log(typeof expressionConst);   // ✗ ReferenceError

function declared() {}
var expressionVar = function () {};
const expressionConst = () => {};

A case that surprises people: inside a block, in strict mode, function declarations have block scope.

'use strict';

if (true) {
  function helper() { return 'inside'; }
}
console.log(typeof helper);   // 'undefined' in strict mode

The practical conclusion: do not declare functions inside blocks. If you need a conditional function, use an expression assigned to a variable declared outside.

  1. Hoisting of var

A var is hoisted without its value: it exists from the start of the function scope, holding undefined.

function calculateHeadroom(openHours) {
  console.log(limit);           // undefined  ← no error, but no value either
  var limit = 40;
  console.log(limit);           // 40
  return limit - openHours;
}

calculateHeadroom(25);

It is equivalent to having written:

function calculateHeadroom(openHours) {
  var limit;                    // ← hoisted by the creation phase
  console.log(limit);           // undefined
  limit = 40;                   // ← the assignment stays where it was
  console.log(limit);
  return limit - openHours;
}

This behavior is the origin of a whole class of silent bugs: the program does not fail, it simply calculates with undefined and produces NaN further down the line. Combined with the function scope you saw in Scope and Closures, it leads to situations like this one:

function analyze(statuses) {
  for (var i = 0; i < statuses.length; i++) {
    if (statuses[i] === 'done') {
      var completed = (completed ?? 0) + 1;   // does it work? yes, but it is unreadable
    }
  }
  return completed;    // visible outside the if and the for: function scope
}

With let and const none of this happens, because the scope is the block and early access raises an error.

  1. let and const: the temporal dead zone

let and const are hoisted too: the engine registers their names during the creation phase. What they do not do is get initialized. They stay in a special state called the temporal dead zone (TDZ), which runs from the start of the scope to the declaration line.

{
  //  ┌─ start of `priority`'s TDZ
  console.log(priority);       // ✗ ReferenceError: Cannot access 'priority' before initialization
  const priority = 'high';
  //  └─ end of the TDZ
  console.log(priority);       // 'high'
}
flowchart LR
    A["Start of the scope"] -->|TDZ: accessing throws a ReferenceError| B["The declaration line"]
    B -->|Initialized| C["Rest of the scope: normal use"]

The TDZ is not a whim: it is a protection. It turns into an immediate error what with var was a silent undefined. And it proves that hoisting exists, because the message tells two situations apart:

console.log(notDeclared);      // ✗ ReferenceError: notDeclared is not defined
console.log(inTdz);            // ✗ ReferenceError: Cannot access 'inTdz' before initialization
let inTdz = 1;

"is not defined" = the name does not exist anywhere. "Cannot access before initialization" = the name exists, but it is still in its TDZ. Knowing how to read that difference saves a lot of debugging time.

One final detail: typeof does not protect you from the TDZ.

console.log(typeof nonExistent);   // 'undefined'  ← safe
console.log(typeof inTdz);         // ✗ ReferenceError
let inTdz = 1;

  1. The complete comparison table

function f(){} var x let x const x
Is the name hoisted? Yes Yes Yes Yes
Is the value hoisted? Yes, complete No (undefined) No (TDZ) No (TDZ)
Use before the line Works undefined ReferenceError ReferenceError
Scope Block (strict) Function Block Block
Redeclarable Yes Yes No No
Reassignable Yes Yes Yes No
Initialization required No No Yes
Added to globalThis (top level) Yes Yes No No

  1. Annotated traces, step by step

Let us analyze a complete file, simulating what the engine does. Here is the code:

'use strict';

console.log('1:', typeof priorityWeight);    // ?
console.log('2:', typeof format);            // ?
console.log('3:', taskCounter);              // ?
// console.log('4:', TODAY);                 // would throw a ReferenceError

var taskCounter = 0;
const TODAY = '2026-09-20';

function priorityWeight(priority) {
  return priority === 'high' ? 3 : 1;
}

const format = (t) => t.toUpperCase();

console.log('5:', typeof format);            // ?
console.log('6:', taskCounter);              // ?
console.log('7:', TODAY);                    // ?

Creation phase of the global scope:

Name State after the creation phase
priorityWeight Complete function, ready
taskCounter undefined (it is a var)
TODAY Uninitialized (TDZ)
format Uninitialized (TDZ)

Execution phase, line by line:

1: 'function'      → the declaration was ready from the start
2: 'undefined'     → ✗ NO: format is in the TDZ; typeof THROWS a ReferenceError

This is worth stopping at, because the example has a deliberate trap: line 2 does not print 'undefined', it throws an error, because typeof does not protect you from the TDZ. Fixing the file so that it runs:

'use strict';

console.log('1:', typeof priorityWeight);    // 1: function
console.log('3:', taskCounter);              // 3: undefined

var taskCounter = 0;
const TODAY = '2026-09-20';

function priorityWeight(priority) {
  return priority === 'high' ? 3 : 1;
}

const format = (t) => t.toUpperCase();

console.log('5:', typeof format);            // 5: function
console.log('6:', taskCounter);              // 6: 0
console.log('7:', TODAY);                    // 7: 2026-09-20

The complete output:

1: function
3: undefined
5: function
6: 0
7: 2026-09-20

The moral of the exercise is not to memorize the table, it is this: any file that depends on these subtleties is badly organized.

  1. The global execution context

An execution context is the internal structure the engine creates to run a piece of code. It holds three things:

Component What it stores
Variable environment The variables and functions declared in that scope
Reference to the outer environment The link that forms the scope chain from 03-04
The value of this The context object (the subject of 04-02)

The first one created is the global context, one per file or script. In the browser, its top-level this is window (or undefined inside functions in strict mode); in Node.js it is module.exports or {} depending on the module type.

A practical consequence you notice straight away:

var withVar = 'reachable from window';
let withLet = 'not reachable from window';

// In the browser, in a classic <script>:
console.log(window.withVar);   // 'reachable from window'
console.log(window.withLet);   // undefined

That is why var at global level is dangerous: it pollutes the global object and can clobber existing properties. let and const do not.

  1. Function contexts

Every call —not every function, every call— creates a fresh context with its own two phases.

function summarizeWorkload(assignee, hours) {
  //  Creation phase of THIS context:
  //    assignee = 'Iván', hours = 25   (the parameters arrive already assigned)
  //    headroom = uninitialized (TDZ)
  //    label    = uninitialized (TDZ)

  const headroom = 40 - hours;
  const label = headroom < 0 ? 'OVERLOADED' : 'OK';
  return `${assignee}: ${headroom} h of headroom — ${label}`;
}

console.log(summarizeWorkload('Iván', 25));    // Iván: 15 h of headroom — OK
console.log(summarizeWorkload('Lucía', 14));   // Lucía: 26 h of headroom — OK

The two calls create two completely independent contexts, with their own headroom and label, which are destroyed when they finish. That is exactly the mechanism that explained in 03-04 why local variables are not shared between calls… unless a closure keeps them alive.

  1. The call stack

Contexts stack up. The call stack is the structure where the engine keeps track of which contexts are active, following the LIFO rule: last in, first out.

'use strict';

function priorityWeight(priority) {
  if (priority === 'high') return 3;
  if (priority === 'medium') return 2;
  return 1;
}

function taskEffort(priority, hours) {
  return priorityWeight(priority) * hours;
}

function backlogReport(priorities, hours) {
  let total = 0;
  for (let i = 0; i < hours.length; i++) {
    total += taskEffort(priorities[i], hours[i]);
  }
  return total;
}

console.log(backlogReport(
  ['high', 'medium', 'high', 'low', 'medium', 'high'],
  [12, 6, 14, 3, 8, 5]
));   // 124
sequenceDiagram
    participant G as Global context
    participant I as backlogReport
    participant E as taskEffort
    participant P as priorityWeight

    G->>I: call
    activate I
    loop 6 times
        I->>E: call
        activate E
        E->>P: call
        activate P
        P-->>E: returns the weight
        deactivate P
        E-->>I: returns weight × hours
        deactivate E
    end
    I-->>G: returns 124
    deactivate I

At the deepest point there are four contexts on the stack: global → backlogReporttaskEffortpriorityWeight. And only four: the loop does not accumulate contexts, because each call closes before the next one starts.

Two important properties of the stack:

  1. It is single-threaded. JavaScript runs one thing at a time. While there is anything on the stack, nothing else can run: not a timer, not a click. That restriction is the whole reason for the event loop you will study in The Event Loop and the Microtask Queue.
  2. It has a limited size. And out of that comes the error in the next section.

  1. RangeError: Maximum call stack size exceeded

If calls pile up without any of them finishing, the stack fills:

function countRemainingTasks(n) {
  return countRemainingTasks(n - 1);   // it never stops
}

countRemainingTasks(6);
// ✗ RangeError: Maximum call stack size exceeded

The limit is around 10,000-15,000 calls depending on the engine. The three usual causes:

Cause Example Fix
Recursion with no base case The one above Add the stopping condition
An unreachable base case if (n === 0) called with n = 5.5 Use n <= 0
Accidental mutual recursion a() calls b(), which calls a() Review the cycle

You can check the limit in your own environment:

function measureDepth(n = 1) {
  try {
    return measureDepth(n + 1);
  } catch (error) {
    return n;
  }
}

console.log(`Maximum depth: ${measureDepth()}`);
// Maximum depth: 11373  (it varies with the engine and the browser)

This error is the most frequent sign of badly written recursion, and that is why you will come back to it in Recursion.

  1. Reading a real stack trace

When something fails, an Error's stack is a snapshot of the stack at that instant. Let us trigger a real failure:

'use strict';

function priorityWeight(priority) {
  return priority.toLowerCase() === 'high' ? 3 : 1;   // ← blows up if it is null
}

function taskEffort(priority, hours) {
  return priorityWeight(priority) * hours;
}

function backlogReport(priorities, hours) {
  let total = 0;
  for (let i = 0; i < hours.length; i++) {
    total += taskEffort(priorities[i], hours[i]);
  }
  return total;
}

console.log(backlogReport(['high', null], [12, 6]));

The trace in Node.js:

TypeError: Cannot read properties of null (reading 'toLowerCase')
    at priorityWeight (/nomada-tasks/js/app.js:4:20)
    at taskEffort (/nomada-tasks/js/app.js:8:10)
    at backlogReport (/nomada-tasks/js/app.js:14:14)
    at Object.<anonymous> (/nomada-tasks/js/app.js:19:13)

How to read it, top to bottom:

Line Meaning
TypeError: Cannot read properties of null What happened
at priorityWeight (…:4:20) Where it blew up: file, line 4, column 20
at taskEffort (…:8:10) Who called it
at backlogReport (…:14:14) Who called that one
at Object.<anonymous> (…:19:13) The top-level code

Three tips for making the most of it:

  1. The first at line is where it blew up; the cause is usually further down. Here the real fault is not in priorityWeight but in whoever passed it null: it is backlogReport that did not validate the data.
  2. <anonymous> is a function with no name. Another reason to always assign expressions to a named const, as recommended in 03-02.
  3. The numbers are line:column. In the browser they are direct links to the code in DevTools.

The lesson Debugging JavaScript goes deeper into this: breakpoints, inspecting the stack live, source maps and console.trace().

  1. Case study: organizing js/app.js

Here is Nómada Tasks's js/app.js written in the worst possible order. It works halfway, and that is what makes it dangerous.

// ✗ js/app.js — BADLY ORDERED
'use strict';

// 1. The configuration is used before it is defined
console.log(`Nómada Tasks board · ${APP_TITLE}`);        // ✗ ReferenceError

const APP_TITLE = 'Taller Nómada';

// 2. An expression is called before it is assigned
console.log(formatStatus('done'));                       // ✗ ReferenceError

const formatStatus = (status) => {
  if (status === 'done') return 'Completed';
  return 'Not started';
};

// 3. This one DOES work, and that is why it confuses: it is a declaration
console.log(priorityWeight('high'));                     // 3

function priorityWeight(priority) {
  return priority === 'high' ? 3 : priority === 'medium' ? 2 : 1;
}

// 4. A hoisted var: no error, just undefined
console.log(`Tasks loaded: ${totalTasks}`);              // Tasks loaded: undefined

var totalTasks = 6;

// 5. A function declared inside a block
if (totalTasks > 0) {
  function start() { console.log('Starting up…'); }
}
start();                                                 // ✗ TypeError in strict mode

Five problems in twenty-five lines. And here is the fixed version, the one you will use as a template for the rest of the course:

// ✓ js/app.js — CORRECT ORDER
'use strict';

// ─── 1. Configuration: constants first ─────────────────────────────
const APP_TITLE = 'Taller Nómada';
const TODAY = '2026-09-20';
const WEEKLY_LIMIT = 40;                   // R7

// ─── 2. Data ───────────────────────────────────────────────────────
const titles      = [
  'Redesign the multipurpose room',
  'Signage for the screen-printing workshop',
  'Update the bookings website',
  'Screen-printing ink inventory',
  'Bookbinding guide for residents',
  'Carpentry workshop quote'
];
const assignees   = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const priorities  = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const statuses    = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const hours       = [12, 6, 14, 3, 8, 5];
const dueDates    = ['2026-09-30', '2026-10-15', '2026-10-02',
                     '2026-09-12', '2026-11-05', '2026-09-05'];

// ─── 3. Pure domain functions ──────────────────────────────────────
function priorityWeight(priority) {
  if (priority === 'high') return 3;
  if (priority === 'medium') return 2;
  if (priority === 'low') return 1;
  return 0;
}

function isOverdue(dueDate, status, today) {
  return dueDate < today && status !== 'done';
}

function formatStatus(status) {
  if (status === 'pending') return 'Not started';
  if (status === 'in-progress') return 'Under way';
  if (status === 'done') return 'Completed';
  return 'Unknown';
}

function describeTask(index, today) {
  const warning = isOverdue(dueDates[index], statuses[index], today) ? ' ⚠ OVERDUE' : '';
  return `[${index + 1}] ${titles[index]} · ${assignees[index]} · ` +
         `${formatStatus(statuses[index])} · ${hours[index]} h${warning}`;
}

// ─── 4. Entry point: ALWAYS at the end ─────────────────────────────
function start() {
  console.log(`Nómada Tasks board · ${APP_TITLE}`);

  let totalEffort = 0;
  for (let i = 0; i < titles.length; i++) {
    console.log(describeTask(i, TODAY));
    totalEffort += priorityWeight(priorities[i]) * hours[i];
  }

  console.log(`Weighted effort: ${totalEffort}`);
  console.log(`Weekly limit per person: ${WEEKLY_LIMIT} h`);
}

start();

Output:

Nómada Tasks board · Taller Nómada
[1] Redesign the multipurpose room · Iván · Under way · 12 h
[2] Signage for the screen-printing workshop · Marta · Not started · 6 h
[3] Update the bookings website · Lucía · Not started · 14 h
[4] Screen-printing ink inventory · Marta · Completed · 3 h
[5] Bookbinding guide for residents · Iván · Under way · 8 h
[6] Carpentry workshop quote · Iván · Not started · 5 h ⚠ OVERDUE
Weighted effort: 124
Weekly limit per person: 40 h

The canonical order for a file, worth adopting as a habit:

flowchart TD
    A["1 · 'use strict'"] --> B["2 · Configuration constants"]
    B --> C["3 · Data"]
    C --> D["4 · Helper and domain functions"]
    D --> E["5 · The start() / main() function"]
    E --> F["6 · One single call: start()"]

  1. Why modern style makes hoisting irrelevant

If you compare the two versions from the previous section, the conclusion is obvious: in the well-ordered version, hoisting does not matter at all. No line depends on it. And that is exactly the goal.

Modern practice Which hoisting problem it removes
const by default, let when it changes, var never No hoisting to undefined and no global pollution
Declare before use, always The TDZ is never reached
One single entry point start() called at the end Everything is defined by the time anything runs
Do not declare functions inside blocks No differences between modes or browsers
'use strict' (or ES modules, which are strict already) Early errors instead of odd behavior
ESLint with no-use-before-define The editor warns you before you run anything

Put another way: understanding hoisting is mainly useful for reading other people's code and for interpreting errors; not for writing code that depends on it. It is diagnostic knowledge, not a technique.

Common Mistakes and Tips

1. Believing var is not hoisted because the console.log gives undefined. It is hoisted; what is not hoisted is the assignment.

2. Believing let and const are not hoisted. They are; they simply stay uninitialized until their line. If they were not hoisted, the error would say "is not defined" instead of "cannot access before initialization".

3. Using typeof as a safety net with let/const. It does not protect you from the TDZ.

4. Declaring functions inside if or for. The behavior differs by mode and engine. Use expressions assigned to variables declared outside.

5. Ignoring the order of the stack trace. The top line is where it blew up, not necessarily where the bug is. Read the whole trace.

6. Confusing the call stack with a closure's memory. The contexts on the stack are destroyed when they finish; the environment captured by a closure survives because there is a function referencing it.

7. Tip: if you are unsure about your file's ordering, move everything inside start(). One single entry point called at the end wipes out nearly every ordering problem in one go.

8. Tip: turn on no-use-before-define in your ESLint configuration. It turns these errors into editor warnings before you run anything (08-02).

Exercises

Exercise 1 — Predict the output

Without running the code, state what each line prints or what error it throws, and explain in which phase it is decided.

'use strict';

console.log(a);            // (1)
console.log(f());          // (2)
console.log(g);            // (3)

var a = 1;
function f() { return 'f'; }
let b = 2;
const g = () => 'g';

function test() {
  console.log(c);          // (4)
  console.log(d);          // (5)
  var c = 'c';
  let d = 'd';
}
test();

Exercise 2 — Fix the file

This js/app.js has four problems related to ordering and hoisting. Identify them, explain why they fail and rewrite the whole file following the canonical order.

'use strict';

start();

var counter = 0;

function start() {
  console.log(`Backlog for ${COMPANY}`);
  for (var i = 0; i < 3; i++) {
    logEntry(i);
  }
  console.log(`Entries: ${counter}`);
  console.log(headroom(45));
}

const COMPANY = 'Taller Nómada';

if (true) {
  function logEntry(n) { counter++; console.log(`Entry ${n}`); }
}

const headroom = (hours) => LIMIT - hours;
var LIMIT = 40;

Exercise 3 — Stack depth

Write a function sumHoursRecursive(hours, index = 0) that adds up the backlog's array of hours by calling itself. Then:

  1. Check that it returns 48 with the backlog.
  2. Generate an array of 50,000 hours and call it. Explain what happens and why.
  3. Rewrite it with a loop and explain why that version does not have the problem.

Solutions

Exercise 1

No. Result Phase where it is decided
(1) undefined Creation: var a was registered and initialized to undefined
(2) 'f' Creation: the function declaration was hoisted complete
(3) ReferenceError: Cannot access 'g' before initialization Creation: const g registered but in the TDZ
(4) undefined Creation of test's context: var c set to undefined
(5) ReferenceError let d is in the TDZ inside that context

In practice the program stops at (3) and the following lines never run. To see them, you have to remove or comment out that line, and then it would stop at (5).

Exercise 2

The four problems:

# Problem Consequence
1 start() is called on the first line Inside it reads COMPANY, which is still in the TDZ → ReferenceError
2 logEntry is declared inside an if in strict mode Block scope: outside it is undefinedTypeError
3 headroom is an arrow used before it is assigned ReferenceError
4 LIMIT is a var, and on top of that it is read inside headroom Even if the ordering were fixed, a global var pollutes; it should be const

The fixed version:

'use strict';

// 1. Configuration
const COMPANY = 'Taller Nómada';
const LIMIT = 40;

// 2. Module state
let counter = 0;

// 3. Functions
function logEntry(n) {
  counter = counter + 1;
  console.log(`Entry ${n}`);
}

const headroom = (hours) => LIMIT - hours;

// 4. Entry point
function start() {
  console.log(`Backlog for ${COMPANY}`);
  for (let i = 0; i < 3; i++) {
    logEntry(i);
  }
  console.log(`Entries: ${counter}`);
  console.log(headroom(45));
}

// 5. One single call, at the end
start();

Output:

Backlog for Taller Nómada
Entry 0
Entry 1
Entry 2
Entries: 3
-5

Comment: counter is a let because it changes, COMPANY and LIMIT are const because they do not, and let i replaces var i in the loop. The -5 also reveals a business problem: 45 h exceeds the limit of 40 (R7), something a validation should prevent long before getting here.

Exercise 3

function sumHoursRecursive(hours, index = 0) {
  if (index >= hours.length) return 0;                     // base case
  return hours[index] + sumHoursRecursive(hours, index + 1);
}

// 1)
console.log(sumHoursRecursive([12, 6, 14, 3, 8, 5]));      // 48

// 2)
const many = [];
for (let i = 0; i < 50000; i++) many.push(1);

try {
  console.log(sumHoursRecursive(many));
} catch (error) {
  console.error(`${error.name}: ${error.message}`);
}
// RangeError: Maximum call stack size exceeded

// 3)
function sumHoursIterative(hours) {
  let total = 0;
  for (const h of hours) total += h;
  return total;
}

console.log(sumHoursIterative(many));   // 50000

Explanation: the recursive version stacks 50,001 contexts, because no call can finish until the next one does (the addition happens on the way back). The engine runs out of stack space and throws a RangeError. The iterative version uses one single context and an accumulator variable, so its stack usage is constant no matter how large the array is. This is exactly the trade-off you will analyze in Recursion.

Conclusion

You now know what the engine does before running your code. Every scope goes through a creation phase, where the declarations are registered —functions complete and ready, vars as undefined, and let/const uninitialized— and an execution phase, where the values are assigned line by line. That is the entire mystery of hoisting: nothing moves, the engine simply already knew the names. The temporal dead zone of let and const is not an obstacle but a protection: it turns into an immediate error what with var was a silent undefined, and that is why the message distinguishes between "is not defined" and "cannot access before initialization".

You have also seen the machinery underpinning all of the above: the execution context, with its variable environment, its reference to the outer scope —the chain that explained the closures of 03-04— and its this; the global context, one per script, where var pollutes the global object and let/const do not; and the call stack, single-threaded and limited in size, whose saturation produces the RangeError: Maximum call stack size exceeded. And you know how to read a stack trace: what failed at the top, who called whom below, and that the root cause is almost never on the first line.

The most important part is the practical conclusion: with const by default, functions declared before they are used, no var, no functions inside blocks and a single start() called at the end, hoisting stops mattering. The well-ordered js/app.js in this lesson is the template you will follow for the rest of the course.

With this you close the "how functions work on the inside" part. What comes next is putting them to work. You have already brushed against the idea several times —listMatching(filter) took a function, createAssigneeFilter returned one— without naming it. That name is higher-order functions, and with them you will build, in Higher-Order Functions, your own versions of map, filter and reduce, a composition system and a configurable reporting engine for Taller Nómada.

JavaScript Course: From Beginner to Advanced

Module 1: Introduction to JavaScript

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved