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
- The engine does not run on the first pass
- The creation phase and the execution phase
- Hoisting of function declarations
- Hoisting of
var letandconst: the temporal dead zone- The complete comparison table
- Annotated traces, step by step
- The global execution context
- Function contexts
- The call stack
RangeError: Maximum call stack size exceeded- Reading a real stack trace
- Case study: organizing
js/app.js - Why modern style makes hoisting irrelevant
- Common Mistakes and Tips
- Exercises
- Conclusion
- 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.
- 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.
- 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 existsFunction 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 modeThe practical conclusion: do not declare functions inside blocks. If you need a conditional function, use an expression assigned to a variable declared outside.
- Hoisting of
var
varA 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.
let and const: the temporal dead zone
let and const: the temporal dead zonelet 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;
- 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 |
- 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-20The complete output:
The moral of the exercise is not to memorize the table, it is this: any file that depends on these subtleties is badly organized.
- 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); // undefinedThat is why var at global level is dangerous: it pollutes the global object and can clobber existing properties. let and const do not.
- 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 — OKThe 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.
- 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]
)); // 124sequenceDiagram
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 → backlogReport → taskEffort → priorityWeight. And only four: the loop does not accumulate contexts, because each call closes before the next one starts.
Two important properties of the stack:
- 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.
- It has a limited size. And out of that comes the error in the next section.
RangeError: Maximum call stack size exceeded
RangeError: Maximum call stack size exceededIf 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 exceededThe 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.
- 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:
- The first
atline is where it blew up; the cause is usually further down. Here the real fault is not inpriorityWeightbut in whoever passed itnull: it isbacklogReportthat did not validate the data. <anonymous>is a function with no name. Another reason to always assign expressions to a namedconst, as recommended in 03-02.- 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().
- Case study: organizing
js/app.js
js/app.jsHere 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 modeFive 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()"]
- 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:
- Check that it returns 48 with the backlog.
- Generate an array of 50,000 hours and call it. Explain what happens and why.
- 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 undefined → TypeError |
| 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:
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)); // 50000Explanation: 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
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
