There is one way of solving problems with functions you have not used on purpose yet, and it is the most disconcerting of all: a function that calls itself. You have already run into it twice —the named function expression in 03-02 existed partly for this, and the RangeError: Maximum call stack size exceeded in 03-05 turned up because of a call that never stopped. In this lesson you will approach it methodically. And it is not an academic exercise: Nómada Tasks has a problem that loops solve badly and recursion solves naturally, because Marta wants a task to be breakable into subtasks, and those subtasks into others, with no depth limit.
Contents
- What a recursive function is
- The base case and the recursive case
- The call stack of a recursion
- Warm-up 1: factorial
- Warm-up 2: Fibonacci and its hidden cost
- The real case: nested subtasks
- Walking the tree: adding up hours
- Walking the tree: flattening, counting and searching
- Recursion versus iteration
- Tail recursion and why JavaScript does not optimize it
- Memoization applied to Fibonacci
- When NOT to use recursion
- Common Mistakes and Tips
- Exercises
- Conclusion
- What a recursive function is
A recursive function is one that, inside its body, calls itself with a smaller problem, until it reaches a case simple enough to be solved directly.
function countdown(n) {
if (n <= 0) { // base case: solved without recursion
console.log('Done!');
return;
}
console.log(`${n} tasks left to review…`);
countdown(n - 1); // recursive case: the same problem, smaller
}
countdown(3);
// 3 tasks left to review…
// 2 tasks left to review…
// 1 tasks left to review…
// Done!The idea behind it is a way of thinking, not a syntax trick:
To solve a big problem, assume you already know how to solve the same problem slightly smaller, and combine that result with whatever you have to do here.
This is called the recursive leap of faith, and it is the hardest part. When you write countdown(n - 1) you do not have to picture all the chained calls: you only have to trust that that call does its job properly.
- The base case and the recursive case
Every correct recursive function has exactly two parts:
| Part | What it does | What happens if it is missing or wrong |
|---|---|---|
| Base case | Solves the smallest problem without calling itself again | Infinite recursion → RangeError |
| Recursive case | Calls itself with a smaller problem | The function never progresses or solves nothing |
And two conditions that must always be verified:
- The base case is reachable. It is not enough for it to exist: the argument must get closer to it on every call.
- The problem really does shrink. Calling yourself with the same argument, or a larger one, is an infinite loop with extra steps.
// ✗ Base case unreachable with decimals
function badCount(n) {
if (n === 0) return 'end';
return badCount(n - 1);
}
// badCount(3.5) → 2.5, 1.5, 0.5, -0.5, -1.5… never exactly 0
// ✓ A robust base case
function goodCount(n) {
if (n <= 0) return 'end';
return goodCount(n - 1);
}flowchart TD
A["Call with n"] --> B{"Base case?"}
B -->|yes| C["Return the direct result"]
B -->|no| D["Do the part that belongs here"]
D --> E["Call itself with a smaller problem"]
E --> F["Combine the result and return"]
- The call stack of a recursion
Here we pick the stack back up from Hoisting and the Execution Context. Each recursive call pushes a new context that is not released until the inner call finishes.
function sumUpTo(n) {
if (n <= 0) return 0;
return n + sumUpTo(n - 1);
}
console.log(sumUpTo(4)); // 10sequenceDiagram
participant G as global
participant A as sumUpTo(4)
participant B as sumUpTo(3)
participant C as sumUpTo(2)
participant D as sumUpTo(1)
participant E as sumUpTo(0)
G->>A: call
A->>B: 4 + ?
B->>C: 3 + ?
C->>D: 2 + ?
D->>E: 1 + ?
E-->>D: 0 (base case)
D-->>C: 1 + 0 = 1
C-->>B: 2 + 1 = 3
B-->>A: 3 + 3 = 6
A-->>G: 4 + 6 = 10
Two essential observations:
- The "way down" phase stacks calls without computing anything definitive: each one waits for the result of the next.
- The "way up" phase is where the calculations actually happen, from the inside out.
That is why a recursion 50,000 levels deep blows the stack, as you saw in the final exercise of 03-05: all 50,000 additions are pending at the same time.
- Warm-up 1: factorial
The factorial of n is the product of every integer from 1 to n. Its mathematical definition is already recursive: n! = n × (n-1)!, with 0! = 1.
function factorial(n) {
if (n < 0) throw new RangeError('Factorial is not defined for negative numbers.');
if (n <= 1) return 1; // base case: 0! = 1 and 1! = 1
return n * factorial(n - 1); // recursive case
}
console.log(factorial(0)); // 1
console.log(factorial(5)); // 120
console.log(factorial(6)); // 720A trace of factorial(5):
| Call | Returns | Result |
|---|---|---|
factorial(5) |
5 * factorial(4) |
5 * 24 = 120 |
factorial(4) |
4 * factorial(3) |
4 * 6 = 24 |
factorial(3) |
3 * factorial(2) |
3 * 2 = 6 |
factorial(2) |
2 * factorial(1) |
2 * 1 = 2 |
factorial(1) |
1 (base case) |
1 |
Notice the throw for the negative case: it is the fail-fast approach from Error Handling. Without it, factorial(-1) would head off toward -Infinity stacking contexts and end up in a RangeError, an error that explains nothing.
- Warm-up 2: Fibonacci and its hidden cost
The Fibonacci sequence starts 0, 1, 1, 2, 3, 5, 8, 13…, where each term is the sum of the two before it.
function fibonacci(n) {
if (n < 0) throw new RangeError('n must be 0 or greater.');
if (n === 0) return 0; // base case 1
if (n === 1) return 1; // base case 2
return fibonacci(n - 1) + fibonacci(n - 2); // TWO recursive calls
}
console.log(fibonacci(10)); // 55Elegant and correct. And disastrous in performance, because each call spawns two, and many of them repeat work already done:
flowchart TD
A["fib(5)"] --> B["fib(4)"]
A --> C["fib(3) ①"]
B --> D["fib(3) ②"]
B --> E["fib(2) ①"]
D --> F["fib(2) ②"]
D --> G["fib(1)"]
C --> H["fib(2) ③"]
C --> I["fib(1)"]
fib(3) is computed twice and fib(2) three times, and with larger n the duplication explodes. The number of calls grows exponentially:
let calls = 0;
function countedFibonacci(n) {
calls++;
if (n <= 1) return n;
return countedFibonacci(n - 1) + countedFibonacci(n - 2);
}
calls = 0; countedFibonacci(10); console.log(calls); // 177
calls = 0; countedFibonacci(20); console.log(calls); // 21891
calls = 0; countedFibonacci(30); console.log(calls); // 2692537
calls = 0; countedFibonacci(35); console.log(calls); // 29860703From 10 to 35, the calls go from 177 to nearly 30 million. In section 11 you will fix this with memoization, and the improvement will be spectacular. Keep in mind that fibonacci(35) takes several seconds: you will compare it.
- The real case: nested subtasks
Up to now Nómada Tasks has worked with a flat list. But Marta has asked for something reasonable: that a big task can be broken down into subtasks, and that those subtasks can be broken down in turn. Here is task 1 of the backlog broken out:
flowchart TD
A["1 · Redesign the multipurpose room"] --> B["11 · Measure and draw up the floor plan<br/>3 h"]
A --> C["12 · Choose the furniture"]
A --> D["13 · Paint and assemble<br/>5 h"]
C --> E["121 · Request quotes<br/>2 h"]
C --> F["122 · Visit two suppliers<br/>2 h"]
That structure is a tree, and in JavaScript it is represented with objects containing arrays of objects:
'use strict';
// Note: here there is no way around using nested objects. Objects are covered
// in depth in Module 4; for now, reading properties with a dot is enough.
const redesignTask = {
id: 1,
title: 'Redesign the multipurpose room',
assignee: 'Iván',
status: 'in-progress',
estimatedHours: 0, // 0 = the hours live in the subtasks
subtasks: [
{
id: 11, title: 'Measure and draw up the floor plan', assignee: 'Iván',
status: 'done', estimatedHours: 3, subtasks: []
},
{
id: 12, title: 'Choose the furniture', assignee: 'Marta',
status: 'in-progress', estimatedHours: 0,
subtasks: [
{ id: 121, title: 'Request quotes', assignee: 'Marta',
status: 'done', estimatedHours: 2, subtasks: [] },
{ id: 122, title: 'Visit two suppliers', assignee: 'Marta',
status: 'pending', estimatedHours: 2, subtasks: [] }
]
},
{
id: 13, title: 'Paint and assemble', assignee: 'Iván',
status: 'pending', estimatedHours: 5, subtasks: []
}
]
};Why is a loop not enough here? Because you do not know how many levels there are. With one for you walk the first level; with two nested, the second; with three, the third. But if tomorrow Iván adds a sub-sub-subtask, the code stops working. Recursion, on the other hand, does not need to know: every level is treated exactly like the one before it.
This is the sign that gives a recursive problem away: the structure of the data contains itself. A task contains tasks, a folder contains folders, a comment contains replies that are comments.
- Walking the tree: adding up hours
The first calculation Marta needs: how many hours a task adds up to counting all its subtasks, at any depth.
/**
* Adds up the hours of a task and of all its subtasks, recursively.
* @param {Object} task a node with estimatedHours and subtasks
* @returns {number} the subtree's total hours
*/
function sumTotalHours(task) {
let total = task.estimatedHours; // what this node contributes
for (const subtask of task.subtasks) { // recursive case
total += sumTotalHours(subtask);
}
return total; // implicit base case: with no subtasks the loop never runs
}
console.log(sumTotalHours(redesignTask)); // 12It is worth analyzing why this works:
- The base case is implicit. If
subtasksis[], theformakes no passes at all and the function returns onlytask.estimatedHours. No explicitifis needed, although writing one would be fine too. - Every node does the same thing: it contributes its own hours and asks its children to contribute theirs.
- The result, 12 h, matches the hours task 1 had in the flat backlog of Module 2 (3 + 2 + 2 + 5 = 12). Breaking it down did not change the total.
And a variant that filters: the subtree's open (unfinished) hours.
function sumOpenHours(task) {
let total = task.status !== 'done' ? task.estimatedHours : 0;
for (const subtask of task.subtasks) {
total += sumOpenHours(subtask);
}
return total;
}
console.log(sumOpenHours(redesignTask)); // 7Checking by hand: still open are "Visit two suppliers" (2 h) and "Paint and assemble" (5 h). Total, 7 h. "Measure and draw up the floor plan" (3 h) and "Request quotes" (2 h) are done.
- Walking the tree: flattening, counting and searching
The same scheme solves every operation over the tree. Only what happens at each node changes.
8.1 Flattening the tree into a list
/**
* Returns a flat array with every task in the subtree,
* adding its depth level to each one.
*/
function flattenTasks(task, level = 0) {
const result = [{ id: task.id, title: task.title, level: level,
hours: task.estimatedHours, status: task.status }];
for (const subtask of task.subtasks) {
const children = flattenTasks(subtask, level + 1);
for (const c of children) result.push(c);
}
return result;
}
const flat = flattenTasks(redesignTask);
for (const t of flat) {
const indent = ' '.repeat(t.level);
const badge = t.status === 'done' ? '✓' : '○';
console.log(`${indent}${badge} [${t.id}] ${t.title}${t.hours > 0 ? ` — ${t.hours} h` : ''}`);
}Output:
○ [1] Redesign the multipurpose room
✓ [11] Measure and draw up the floor plan — 3 h
○ [12] Choose the furniture
✓ [121] Request quotes — 2 h
○ [122] Visit two suppliers — 2 h
○ [13] Paint and assemble — 5 hThe level = 0 parameter with a default value (03-03) is a common pattern in recursion: whoever calls from outside omits it, and the function itself increments it on the way in.
8.2 Counting tasks and measuring the depth
function countTasks(task) {
let total = 1; // itself
for (const sub of task.subtasks) total += countTasks(sub);
return total;
}
function maxDepth(task) {
if (task.subtasks.length === 0) return 1; // explicit base case
let deepest = 0;
for (const sub of task.subtasks) {
const d = maxDepth(sub);
if (d > deepest) deepest = d;
}
return 1 + deepest;
}
console.log(countTasks(redesignTask)); // 6
console.log(maxDepth(redesignTask)); // 38.3 Searching for a task by identifier
/**
* Searches the whole subtree for a task by id.
* @returns {Object|null} the task found, or null
*/
function findById(task, id) {
if (task.id === id) return task; // base case 1: found
for (const sub of task.subtasks) {
const found = findById(sub, id);
if (found !== null) return found; // stops as soon as it is found
}
return null; // base case 2: not in this branch
}
const t = findById(redesignTask, 122);
console.log(t.title); // Visit two suppliers
console.log(findById(redesignTask, 999)); // nullThe if (found !== null) return found; matters: without it, the function would carry on exploring useless branches after having found the result. It is the recursive equivalent of the break from break, continue and Nested Loops.
- Recursion versus iteration
Anything you can do with recursion you can do with loops, and the other way round. The question is which one suits each case.
| Criterion | Recursion | Iteration (loops) |
|---|---|---|
| Readability with nested data | Very high: the code mirrors the structure | Low: you have to manage a stack by hand |
| Readability with flat data | Worse: it adds noise | High |
| Memory | One stack entry per call | Constant |
| Risk of overflow | Real beyond ~10,000 levels | None |
| Speed | Slightly lower (each call costs) | Slightly higher |
| Debugging | Harder: the stack fills with identical frames | Straightforward |
| State | Implicit, in the parameters | Explicit, in variables |
Compare the two versions of the same task, flattening the tree. You have already seen the recursive one. The iterative one needs to manage its own stack:
function flattenIterative(root) {
const result = [];
const pending = [{ task: root, level: 0 }]; // explicit stack
while (pending.length > 0) {
const current = pending.pop();
result.push({ id: current.task.id, title: current.task.title, level: current.level });
// They are pushed in reverse order so they come out in the natural order
for (let i = current.task.subtasks.length - 1; i >= 0; i--) {
pending.push({ task: current.task.subtasks[i], level: current.level + 1 });
}
}
return result;
}
console.log(flattenIterative(redesignTask).length); // 6It works, it cannot overflow the engine's stack and it is faster. But look at what it cost: a manual stack, a not-very-obvious reverse loop and a helper object to carry the level. That is the real choice: clarity versus control.
A practical day-to-day rule:
- A nested structure of moderate depth (menu trees, comments, subtasks, JSON) → recursion.
- A flat list, or potentially enormous depth (thousands of levels, large files) → iteration.
- Tail recursion and why JavaScript does not optimize it
A recursion is said to be a tail recursion when the recursive call is the last thing the function does: its result is returned as it stands, with no pending operations.
// NOT a tail call: on the way back there is still a multiplication to do
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // ← the multiplication is left pending
}
// A tail call: the call is returned directly
function tailFactorial(n, accumulated = 1) {
if (n <= 1) return accumulated;
return tailFactorial(n - 1, n * accumulated); // ← nothing pending
}
console.log(tailFactorial(5)); // 120In theory, a tail recursion does not need to stack contexts: since there is nothing left to do on the way back, the engine could reuse the current frame. That is called tail call optimization (TCO) and it would turn the recursion into a loop, with constant memory.
The problem is that, in practice, JavaScript does not apply it:
| Engine / environment | Does it optimize tail calls? |
|---|---|
| The ES2015 specification | Yes, it requires it |
| V8 (Chrome, Edge, Node.js) | No (implemented and then removed) |
| SpiderMonkey (Firefox) | No |
| JavaScriptCore (Safari) | Yes, in strict mode |
Check it for yourself:
function tailCount(n) {
if (n <= 0) return 'end';
return tailCount(n - 1);
}
try {
console.log(tailCount(100000));
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
// In Node.js: RangeError: Maximum call stack size exceededThe practical conclusion: writing tail recursion in JavaScript does not protect you from overflow. It is a good stylistic habit and it works in other languages, but here, if the depth could get large, the solution is a loop.
- Memoization applied to Fibonacci
Back to the exponential Fibonacci from section 5. The problem was recomputing the same thing over and over, and you already have the tool to fix it: memoization with a closure from Scope and Closures.
/**
* Returns a memoized version of Fibonacci.
* The cache lives in the closure: private and persistent between calls.
*/
function createFibonacci() {
const cache = {}; // private state
let computations = 0;
let hits = 0;
function fib(n) {
if (n < 0) throw new RangeError('n must be 0 or greater.');
if (n <= 1) return n;
if (n in cache) {
hits++;
return cache[n];
}
computations++;
const result = fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
}
fib.stats = () => `${computations} computations, ${hits} hits`;
return fib;
}
const fastFibonacci = createFibonacci();
let t = Date.now();
console.log(fastFibonacci(35), `${Date.now() - t} ms`); // 9227465 ~0 ms
console.log(fastFibonacci.stats()); // 34 computations, 33 hits
t = Date.now();
console.log(fastFibonacci(40), `${Date.now() - t} ms`); // 102334155 ~0 ms
console.log(fastFibonacci(90)); // 2880067194370816000The comparison is devastating:
n |
Calls without memoization | Computations with memoization | Approximate time |
|---|---|---|---|
| 10 | 177 | 9 | ~0 ms in both |
| 20 | 21,891 | 19 | ~0 ms in both |
| 30 | 2,692,537 | 29 | ~30 ms → ~0 ms |
| 35 | 29,860,703 | 34 | ~300 ms → ~0 ms |
| 40 | ~331,000,000 | 39 | several seconds → ~0 ms |
| 50 | unfeasible | 49 | — → ~0 ms |
You go from exponential growth to linear growth. The cost is the cache's memory, which here is negligible, and the usual restriction: memoize pure functions only (03-03). fib(n) always gives the same thing for the same n, so it is safe.
Two warnings about the result of fastFibonacci(90): that number exceeds Number.MAX_SAFE_INTEGER and is therefore approximate. For large integers you have to use BigInt, the type you met in Variables and Data Types. And the depth is still linear: fastFibonacci(20000) would overflow the stack even with the cache working.
- When NOT to use recursion
There are three situations where recursion is the wrong answer:
| Situation | Why | Alternative |
|---|---|---|
| Walking a flat list | A loop is clearer, faster and safer | for / for...of |
| Large or unbounded unknown depth | RangeError in production, with real data |
Iteration with an explicit stack |
| A simple calculation with an accumulator | Recursion adds noise and contributes nothing | A loop with an accumulator variable |
// ✗ Unnecessary recursion over a flat list
function sumHoursRecursive(hours, i = 0) {
if (i >= hours.length) return 0;
return hours[i] + sumHoursRecursive(hours, i + 1);
}
// ✓ A loop: clearer, with no stack risk
function sumHours(hours) {
let total = 0;
for (const h of hours) total += h;
return total;
}
console.log(sumHours([12, 6, 14, 3, 8, 5])); // 48And a security warning that matters more than it looks: if the nested data comes from outside (an API, a file the user uploads), the depth is controlled by whoever sends the data. A malicious tree 100,000 levels deep would take your recursive function down. In those cases, either you iterate, or you set an explicit limit:
function sumTotalHours(task, depth = 0) {
if (depth > 50) {
throw new RangeError('The subtask structure is too deep (maximum 50 levels).');
}
let total = task.estimatedHours;
for (const sub of task.subtasks) {
total += sumTotalHours(sub, depth + 1);
}
return total;
}Common Mistakes and Tips
1. Forgetting the base case. It is mistake number one and it always produces RangeError: Maximum call stack size exceeded.
2. An unreachable base case. Use <= instead of === when the argument could skip over it (decimals, subtracting more than one).
3. Not shrinking the problem.
4. Forgetting the return in front of the recursive call.
function badFind(task, id) {
if (task.id === id) return task;
for (const sub of task.subtasks) {
badFind(sub, id); // ✗ the result is thrown away
}
return null; // always null
}It is a silent failure: no error, it simply never finds anything.
5. Confusing a for inside a recursion with a nested loop. The for walks this node's children; the recursion goes down a level. They are different axes.
6. Memoizing an impure function. You have known this since 03-04: the cache would hand back stale results.
7. Relying on tail recursion. It is not optimized in most engines.
8. Tip: draw the call tree for three or four levels. Nearly every recursion bug is obvious at a glance in the drawing and invisible when reading the code.
9. Tip: add an indented console.log when debugging.
function sumWithTrace(task, level = 0) {
const indent = ' '.repeat(level);
console.log(`${indent}→ entering ${task.title}`);
let total = task.estimatedHours;
for (const sub of task.subtasks) total += sumWithTrace(sub, level + 1);
console.log(`${indent}← leaving ${task.title} with ${total} h`);
return total;
}Seeing the way down and the way up indented makes any recursion comprehensible. In Debugging JavaScript you will see how to do the same thing with breakpoints and the live stack.
Exercises
Exercise 1 — Operations over the subtask tree
Over redesignTask, write three recursive functions:
countByStatus(task, status): how many tasks in the subtree are in that status.titlesForAssignee(task, name): an array with the titles of the tasks assigned to that person, at any depth.isComplete(task):trueif the task and all its subtasks are in status'done'.
Exercise 2 — The path to a task
Write pathTo(task, id) returning an array with the titles running from the root down to the task with that identifier, or null if it does not exist. For example, for id 121 it must return ['Redesign the multipurpose room', 'Choose the furniture', 'Request quotes'].
Exercise 3 — From recursive to iterative
The maxDepth function from section 8.2 is recursive. Rewrite it iteratively using an explicit stack, check that it gives the same result (3) and explain in which specific case you would prefer each version.
Solutions
Exercise 1
function countByStatus(task, status) {
let total = task.status === status ? 1 : 0;
for (const sub of task.subtasks) {
total += countByStatus(sub, status);
}
return total;
}
function titlesForAssignee(task, name) {
const found = [];
if (task.assignee === name) found.push(task.title);
for (const sub of task.subtasks) {
const children = titlesForAssignee(sub, name);
for (const c of children) found.push(c);
}
return found;
}
function isComplete(task) {
if (task.status !== 'done') return false; // guard: this one is not done
for (const sub of task.subtasks) {
if (!isComplete(sub)) return false; // stops at the first one that fails
}
return true;
}
console.log(countByStatus(redesignTask, 'done')); // 2
console.log(countByStatus(redesignTask, 'pending')); // 2
console.log(countByStatus(redesignTask, 'in-progress')); // 2
console.log(titlesForAssignee(redesignTask, 'Marta'));
// [ 'Choose the furniture', 'Request quotes', 'Visit two suppliers' ]
console.log(isComplete(redesignTask)); // false
console.log(isComplete(findById(redesignTask, 11))); // trueComment: all three follow the same scheme —do something with this node, then with each child— and only the operation changes. isComplete also builds in an early exit: as soon as it finds an incomplete subtask it stops looking at the rest, just like myEvery in Higher-Order Functions.
Exercise 2
function pathTo(task, id) {
if (task.id === id) return [task.title]; // base case: this is the one
for (const sub of task.subtasks) {
const childPath = pathTo(sub, id); // the leap of faith
if (childPath !== null) {
return [task.title, ...childPath]; // put this level at the front
}
}
return null; // not in this branch
}
console.log(pathTo(redesignTask, 121));
// [ 'Redesign the multipurpose room', 'Choose the furniture', 'Request quotes' ]
console.log(pathTo(redesignTask, 13));
// [ 'Redesign the multipurpose room', 'Paint and assemble' ]
console.log(pathTo(redesignTask, 999)); // null
// And to display it as a breadcrumb trail:
const path = pathTo(redesignTask, 122);
console.log(path.join(' › '));
// Redesign the multipurpose room › Choose the furniture › Visit two suppliersComment: the key is the line return [task.title, ...childPath];. The ... is the spread operator, which unfolds the elements of childPath inside the new array; you will study it formally in Object Destructuring, Spread and Rest. Without it, you would have to build the array with a loop. Notice too that the path is assembled on the way up: each level adds its title in front of whatever the level below hands back.
Exercise 3
function maxDepthIterative(root) {
let deepest = 0;
const pending = [{ task: root, level: 1 }];
while (pending.length > 0) {
const current = pending.pop();
if (current.level > deepest) deepest = current.level;
for (const sub of current.task.subtasks) {
pending.push({ task: sub, level: current.level + 1 });
}
}
return deepest;
}
console.log(maxDepth(redesignTask)); // 3
console.log(maxDepthIterative(redesignTask)); // 3When to prefer each one:
| Version | When |
|---|---|
| Recursive | Subtask trees created by the Taller Nómada team: two or three levels deep, and the code reads like the definition of the problem |
| Iterative | Data imported from an external API, where the depth is not under your control and a very deep tree would take the application down |
Notice one subtle difference as well: the recursive version computes the depth on the way up (1 + the maximum of the children), while the iterative one carries it on the way down, storing the level alongside each pending node. It is the same calculation seen from the other side.
Conclusion
With this lesson you close Module 3. You now know that a recursive function calls itself with a smaller problem, that it always needs a reachable base case and a recursive case that genuinely shrinks the problem, and that each call stacks a context that is only released on the way back up: that is why a deep recursion produces the RangeError you met in 03-05. You have practiced with factorial and fibonacci, and you have seen first-hand how the naive Fibonacci goes from 177 calls at n = 10 to nearly thirty million at n = 35, and how the closure-based memoization from 03-04 brings it back down to thirty-four computations.
Above all, you have seen why recursion exists. When Marta asked for a task to be broken down into subtasks, and those into others, loops stopped working: you cannot write a nested for for a depth you do not know. sumTotalHours, flattenTasks, countTasks, maxDepth, findById and pathTo solve that tree with the same six-line scheme, because the structure of the code mirrors the structure of the data. And you know its limits: the recursion-versus-iteration table, the tail recursion JavaScript does not optimize, the depth limit for data coming from outside and the three situations in which a loop is simply the right answer.
Look at what you have gained over these seven lessons. You started with blocks of code copied four times and you finish with priorityWeight(), isOverdue(), describeTask(), createTask(), summarizeWorkload(), createIdGenerator(), createTaskStore(), a configurable reporting engine and a recursive walk over subtasks. You know how to define and call functions, how to write them as values and as arrows, how to design their parameters and their return values, how to control where each variable lives, how to lock state inside a closure, how to understand what the engine prepares before running anything, how to pass functions to other functions and how to make a function call itself. That is, by a long way, the biggest leap of the course so far.
But notice the price you have been paying without complaining. This whole module has worked with loose variables and parallel arrays: titles[i], assignees[i], priorities[i], statuses[i], hours[i], dueDates[i]. You have had to filter indices instead of tasks, pass eight arguments to describeTask, drag six arrays into every function and trust that none of them ever gets out of alignment. And the moment a real structure showed up —the subtask tree— there was no option but to use nested objects, with a note saying that would come later. That "later" is now. In Module 4: Objects and Arrays you will stop simulating the data model and write it properly: each task will be an object with its nine fields, the backlog will be an array of objects, and every function from this module will be rewritten with clean signatures like describeTask(task, today). Start with Introduction to Objects, and the first relief will arrive on the very first page.
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
