Guide to understanding recursion
As itâ€™s one of the most frequently asked questions on the forum Iâ€™ve decided to answer here, so all of you who have hard times with recursion can easily find it.
Part 1. Precedence
First step to understand recursion requires understanding of operator precedence in JavaScript:
Operator precedence determines how operators are parsed concerning each other. Operators with higher precedence become the operands of operators with lower precedence.
Source: https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
What itâ€™s saying is if you have this code:
const a = 42 + someFunction() / 2;
JS parser will have to have certain rules of what to evaluate first. Letâ€™s check precedence table from the link above and parse this statement:

20: Function Call
Call someFunction()
and substitute the return value
*(letâ€™s say someFunction()
returns 100):
const a = 42 + 100 / 2;

15: Division
as we know from the school division will go before the addition
const a = 42 + 50;
const a = 92;
at this point parser will assign 92 to a constant variable a
;
You might ask what does precedence have to do with the recursion? Everything! It is a direct side effect of precedence:
function recurseForever(a) {
return recurseForever(a + 1);
}
Outer function recurseForever
will return NOT the inner function, BUT the result of it! There is no special logic or mode in the language that allows recursion, no! Itâ€™s simply an order in which things get evaluated by parser.
Part 2. Stack
If you try to run recurseForever
function from the example above, of course, it wonâ€™t run forever, in fact it would very quickly throw an error:
Uncaught RangeError: Maximum call stack size exceeded
Whenever you see something like this, it means itâ€™s time to debug! And the first step of any debugging is to understand a context of an Error. After reading about RangeError and Call Stack weâ€™re coming to conclusion that we have this internal structure called â€śCall Stackâ€ť with some limited size (Range) and whenever we exceed the size we will get this RangeError
, good!
Now when we know the context of a problem (â€śWHATâ€ť), itâ€™s time to search for a reason (â€śWHYâ€ť). Whenever a JavaScript engine runs a function it pushes it to the Call Stack. Think of Call Stack as an primitive array with just a two methods:

push(fn)
 pushes function to the stack. Invoked whenever it sees the function call

pop()
 removes the last function from the stack. Invoked whenever the last statement of the current function is executed. Letâ€™s simulate that with pseudofunction parseLine
:
const callStack = [];
parseLine('recurseForever(0);'); // Oh, it's a function call!
callStack.push(recurseForever);
parseLine('return recurseForever(a + 1);');
/**
* Ok, as we already know from Precedence part,
* the first thing we will deal with here is recurseForever(a + 1)
* which is another function call!
*/
callStack.push(recurseForever);
parseLine('return recurseForever(a + 1);');
/**
* And here's the BUG!
* Have you noticed it?
*/
We went back to the exact same argument and without simulating further it must be clearly understandable that we will keep pushing our function to the call stack until itâ€™s size is exceeded.
Now, letâ€™s fix it!
Part 3. Iteration
Before we fix our recursive function though, letâ€™s quickly consider one quick thing first, because when I said this just now:
We went back to the exact same argument
I actually lied!
In every function call we indeed have new arguments. We started with 0, then inside the function body we called our function with a + 1
, so 1, then 2, 3 â€¦ and so on. To verify that letâ€™s log an argument:
function recurseForever(a) {
console.log(a);
return recurseForever(a + 1);
}
recurseForever(0);
// Ok, now we know the size of a call stack
And NOW pattern recognition abilities of your brain must start ringing bells: â€śWait a minute  this looks just like a loop! Weâ€™re simply iterating!â€ť. And youâ€™re right (even if that was just my bells). We can very easily â€śconvertâ€ť our function to a loop:
// Don't run this
function recurseReallyForever(a) {
while ("We can") {
a += 1;
}
return a;
}
The only exception here is that our call stack in the case of recurseReallyForever(a)
will not be exceeded  it only will have one outer function, because there are no function calls inside. Instead there is a while
loop inside that now will truly run forever and crush our browser.
And this is a point of midway takeaways!
Takeaway 1:
Whenever you â€śattachâ€ť parentheses to a function name itâ€™s no longer a function, but a result of the function:
function fortyTwo() {
return 42;
}
typeof fortyTwo; // "function"
typeof fortyTwo(); // "number"
Takeaway 2:
Recursion is nothing else than iteration that doesnâ€™t use iterable data structures (like array), but instead uses an arraylike system structure called Call Stack. The natural function of recursion is to repeat itself over some set of changing arguments.
Part 4. Base case
Now finally, with all the knowledge weâ€™ve just read, letâ€™s fix our bug! In order to do that, in some point of the iterations of our initial recurseForever
function we need a condition in which we no longer make a function call  an exit contract. We call such a condition â€śBase caseâ€ť. Every recursive function has to have one, otherwise it will cause a problem and as general convention we normally put base case as a first thing in the recursive function:
function recurseForever(a) {
// base case
if (a >= 10) return a;
console.log(a);
return recurseForever(a + 1);
}
Congratulations! Base case isnâ€™t exclusively a part of recursive function, it is as well a part of any iteration  exit condition out of iteration and you use it all the time with loops etc.
Part 5. Result
If you run the final version of our function recurseForever(0)
you will get what seems like logs from 0
to 10
and the first naive guess that they came from console.log(a)
method, right? But why was 10
logged? Number 10 satisfies our base case and therefore console.log(a)
with a = 10
never was invoked, instead 10
is a result of our function that was sent to stdout
because we havenâ€™t instructed to assign this value to anything and thatâ€™s a default behaviour. Compare outputs:
recurseForever(0); // logs 0 ... 10
const result = recurseForever(0); // logs 0 ... 9
console.log(result); // And that's where 10 did go!
And now I need maximum focus! Because itâ€™s the core of all the confusion.
"How the hell result
equals 10?!!"
Magic, ha! There are two ways to understand what has just happened:
Logical
I call this a â€ślogical traversal of recursive callâ€ť, though itâ€™s not an official or conventional term. The key is to start from the end and iterate backwards. Whatâ€™s the (successful) end of any recursion? Base case!
// What's the output of the following?
recurseForever(10);
// Easy  10!
Now iterating backwards:
// What's the output of the following?
recurseForever(9);
// We're skipping base case
// and returning recurseForever(9 + 1);
Now taking into consideration the takeaway 1 from the above, whatâ€™s recurseForever(9 + 1)
? Itâ€™s no longer a function, itâ€™s a value  10!
Now continue the same process:
// What's the output of the following?
recurseForever(8);
// recurseForever(8 + 1), which we've just calculated
â€¦and letâ€™s stop here actually. We clearly see a recurring pattern here that kinda suggests following:
"If we can get the next recursive iteration after the base case to work, then our recursive function works!"
So coming back to the countdown()
function from the thread, our base case would be 1
because itâ€™s 100% clear how to make a countdown from 1, right?
countdown(1);
/* this shall immediately return [1] without recursion */
Now all we need to do is to take the next after the base case input  2 and produce [2, 1]
output in a recursive way and that would guarantee the rest of the cases! Pretty neat! Weâ€™ll get back to this!
Technical
To understand how exactly this works under the hood we need to get back to the Call Stack. As weâ€™ve seen any recursion is a process of inflating the stack, until we either hit the limit of the stack or the base case occurs. After the case of the latter we will initiate a backward process of deflating the stack, as our imaginary parseLine
function will move to the next line, ending each function and popping it out of stack in reverse order.
Reverse is a key word here and is a result of the nature of the Stack. Imagine a stack of cards: inflating the stack would mean taking cards and stack them on top of each other one by one: push()
. Deflating stack would mean taking cards out of the stack from the top until you reach the bottom: pop()
. So Last card pushed In would be the First card taken Out (LIFO). The graphical chart where x
is time and y
is a stack size would look like this:
* < base case
***
*****
*******
*********
***********
************* < result
Every row of this graph is a separate instance of our function. Weâ€™ve called only one of them  the one at the bottom, the rest were called subsequently in the process of inflation. Each of these functions carries its own result and passes it down until it reaches our main function at the bottom. Here is the same in the form of timeline:
STEP 1: recurseForever(0) pushed to Call Stack
STEP 2: recurseForever(1) pushed to Call Stack
...
STEP 10: recurseForever(9) pushed to Call Stack
STEP 11: recurseForever(10) pushed to Call Stack
STEP 12: recurseForever(10) popped out of Call Stack carrying 10
STEP 13: recurseForever(9) popped out of Call Stack carrying 10
STEP 14: recurseForever(8) popped out of Call Stack carrying 10
...
STEP 21: recurseForever(1) popped out of Call Stack carrying 10
STEP 22: recurseForever(0) popped out of Call Stack carrying 10
STACK EMPTY
As you can see after our function is getting popped out of the call stack it carries value (we also say â€śitâ€™s evaluated toâ€ť) that now can be assigned, returned or simply printed to stdout
by JavaScript engine.
Here is another visualisation of similar process kindly made by @ieahleen:
Red line on this diagram represent inflation of the call stack and blue line  deflation that carries the output towards the the initial function call.
Part 6. Solution
The best way to understand how the solution works is to solve it
Step 1 (click to show/hide)
So whatâ€™s our input, what do we know? We know that countdown
expects number n
and has to output an array from n
to 1. Right away itâ€™s sufficient for us to write couple lines of code:
function countdown(n) {
// base case
if (n === 1) return [1];
// in some point we need to iterate towards base case
countdown(n  1);
}
Step 2 (click to show/hide)
Now, whatâ€™s the next step after the base case? Number 2. Given input where n = 2
we need to produce the following output: [2, 1]
. Any ideas? We know that countdown(n  1)
will return [1]
. How do we get from [1]
to [2, 1]
? Well, thatâ€™s not hard!
function countdown(n) {
// base case
if (n === 1) return [1];
const output = [n]; // [2]
return output.concat(countdown(n  1)); // [2, 1]
}
Ok, one can say â€śYouâ€™ve hardcoded it with n = 2, cheater!â€ť. What do you think, are we hardcoders? Letâ€™s test it:
countdown(5); // [5, 4, 3, 2, 1]
countdown(7); // [7, 6, 5, 4, 3, 2, 1]
Works!!!
Step 3 (click to show/hide)
Now the challenge has additional requirement: if n less than 1 we shall return an empty array. Letâ€™s add this:
function countdown(n) {
// base case
if (n < 1) return [];
if (n === 1) return [1];
const output = [n]; // [2]
return output.concat(countdown(n  1)); // [2, 1]
}
Then test again:
countdown(0); // []
countdown(33); // []
Yei!
Now letâ€™s refactor a bit. I think we have couple unnecessary lines:
function countdown(n) {
// base case
if (n < 1) return [];
if (n === 1) return [1]; // Do we really need this line?
/**
* To check it all we need is just to simulate all the above
* for input 0 as base case and 1 as a next
* so the return value for n = 1 would be [1].concat([]);
*/
const output = [n]; // Notice how we assign this to output just to return it on the next line. Waste of line!
return output.concat(countdown(n  1));
}
Solution (click to show/hide)
And here is the result:
function countdown(n) {
if (n < 1) return [];
return [n].concat(countdown(n  1));
}
// or using ternary operator
function countdown(n) {
return (n < 1) ? [] : [n].concat(countdown(n  1));
}
Boom!
Part 7. Couple extra famous examples
Factorial (easy)
function factorial(n) {}
STEP 1: Whatâ€™s factorial? Itâ€™s 1 * 2 * 3 * â€¦ * n.
STEP 2: Base case and iteration? Looking at the above, if we have n
then we go to 1: (n  1) until n === 1
STEP 3: Write it:
function factorial(n) {
// considering the fact that 1 * n = n
if (n === 1) return n;
return factorial(n  1);
}
STEP 4: Produce the next result after base case: factorial(2) > 2
. How to return 2 if we have 1?
STEP 5: Back to code:
function factorial(n) {
if (n === 1) return n;
return n * factorial(n  1); // 2 * 1  just what we need
}
STEP 6: Test other numbers
factorial(4); // 24 Correct!
factorial(5); // 120 Correct!
Tree traversal (intermediate)
This example is intentionally different to show you other applications of recursive function. Probably the most famous of which is tree traversal.
/**
* Traverses tree from starting node
* until it finds node with given id
* if not found returns null
*/
function getElementById(node, id) {}
Iteration here is a bit different because every node has child nodes under node.children
property, so we need to check all the children and children of those children and so on until either weâ€™ve checked all nodes or found the one weâ€™re looking for. And that would be our base cases:
function getElementById(node, id) {
/* base case # 1 */
if (node.id === id) return node;
/* if not, iterate over each child node repeating the process */
for (const child of node.children) {
/* repeat the same function for child and its children */
const match = getElementById(child, id);
/* if match gave us result  return it immediately */
if (match) return match;
/* if not, continue loop */
/* we cannot return null without checking all children first */
}
/* if loop finishes and we haven't matched node at this point then it's base case #2 */
return null;
}
Part 8. Conclusion
Ok, here are key takeaways:

Understanding recursion requires understanding of many programming principles and thatâ€™s why it might be overwhelming sometimes. Just look at the length of this guide.

Recursion often compared against iteration, but in a nutshell itâ€™s also an iteration (otherwise they would simply be incomparable)

Each iteration of recursive function requires new arguments and the change between previous and the next argument has to point towards the base case
Recursion has very solid structure that is divided in two parts: inflation  all the code before internal function call and deflation  all the code that will run â€śon the way backâ€ť. Understanding and seeing this abstract structures gives you more understanding when you read or compose recursive function.
Good luck!