How the Stack Works and Why it Matters
What is the stack?
When first learning about the stack data structure, it’s helpful to think of an analogy. We’ll start with one that everyone is familiar with — ice cream! When you start making an ice cream, you start off with an empty ice cream cone — an empty stack. Then, you put on your first scoop of ice cream — let’s say, peanut butter. Next, you’ll put on the second scoop — mint (eww). This continues until you’ve loaded up your ice cream cone with several scoops of different flavours.
Now it’s time to eat all that delicious ice cream. Obviously you can’t eat the first scoop you placed on the cone, since it’s supporting the other scoops above. You always eat the top scoop first, which was the last scoop you placed on top. This is a fundamental concept of the stack data structure — first in, last out (FILO). FILO means that whatever sits on the top of the stack is also the first to be removed, and the data resolves in the reverse order to which it is added. You eat the top scoop first, then the next, and the last scoop you eat is the one you placed on the cone first.
This is the core of the stack data structure — you are setting up the data one on top of the other, and resolving them in the reverse order to which you placed them. First in, last out.
Okay, now I’m craving ice cream, but how is this relevant to programming?
The Call Stack
This is an example of how synchronous code with no dependencies operates on the call stack. Since there are no nested callbacks in the code, each “scoop” is loaded on to the cone and instantly “eaten”. The code operates as you’d expect — each piece of code is executed the moment it is called.
How about if there are nested callbacks? This is where the stack model really comes into effect. Let’s look at a slightly more complicated program and how the call stack interacts with it.
Understanding the call stack provides numerous benefits. First, it is invaluable when debugging — understanding the order in which code resolves allows the programmer to identify potential issues before they even occur, and allows controlled use of side effects within functions to alter how your program runs. If you’ve ever used console.log() statements within functions to determine the value of something while debugging, you’ve interacted with the call stack — you intuitively knew that the console.log() statement would be run whenever that function is called as a side effect, and would print to the console before the rest of the function was executed.
Now that we understand how the call stack operates within our language, how can we go about utilizing the model for ourselves? Although there are myriad ways one can utilize a stack data structure, one of the most common is the use of recursion.
Recursion is where a function calls itself continuously until it reaches a “base state”, at which point the recursion ceases and the chain resolves. As you may have guessed, this is simply a stack consisting of multiple calls of the same function, which resolves once the base case is placed on top.
Recursion can be an extremely confusing topic for new programmers, but understanding the stack data structure makes understanding recursion much simpler. Recursion is a function calling itself, and each call brings it closer and closer to the base case — similar to how in the above helloWorld() example, each call brought us closer and closer to a function without a function call — in that case, hello() and world() just returned simple strings. Once we had those pieces of the puzzle, the lower functions on the stack were able to slot them into their own return statements, and a chain of resolving functions finished off the sequence.
Let’s take a look at an example and watch how the call stack operates together.
Let’s say we have an interview question — write a recursive function that takes in a number, and sums all the numbers between that number and 1. For example, if we provided the number 5 as an argument, the program would return 5 + 4 + 3 + 2 + 1 = 15. Here’s an example of a possible recursive solution:
So, how does this work? Well, we know the base case — if the number is 1, the answer is 1. In all other cases, we have to figure out how to get closer to 1.
recursiveFunc(2) = 2 + 1 = 3
recursiveFunc(3) = 3 + 2 + 1 = 6
recursiveFunc(4) = 4 + 3 + 2 + 1 = 10
Notice how each time we go up a number, the result is increasing by that number. That means we can rewrite this as:
recursiveFunc(4) = 4 + recursiveFunc(3)
recursiveFunc(number) = number + recursiveFunc(number - 1)
This is our recursive case. Each time we call the function, the input number is reduced by one for the next call, steadily getting closer and closer to our base case. Let’s run it and watch the call stack.
Recursive algorithms work by utilizing the stack model to constantly call a function until that function reaches a base case. That base case is the missing puzzle piece that then solves the previous puzzle, and that previous puzzle is then used as the missing piece for the puzzle before that. This continues until the whole puzzle has been completed.
As you can see from the above image, the call stack can be heavily loaded before resolving. Note that the call stack is not infinite, and your program can throw a “call stack exceeded” error when too many functions are placed on the stack. This is usually because a recursive algorithm is not able to reach its base case, and infinitely loops through the recursive case. This call stack limit depends on the browser or software running it — at the time of this writing, Chrome supports a call stack limited to 10,300 function calls.
Reaching the Bottom of the Stack (article)
You may still be confused by the call stack and recursion — don’t be discouraged, this is a tough concept to wrap your head around! Take some time to play around with it in the Chrome Dev Tools (Sources >> Snippets to write JS directly into the testing area), or look back on a previous program you wrote and see if you can guess what the call stack will do. Chances are you can, and now you have the knowledge to explain why the program is running in that specific order.
Understanding the stack data structure is an important step towards learning the call stack. Understanding the call stack is an important step towards becoming good with recursion. Becoming good with recursion is an important step towards becoming an excellent programmer. You now understand the stack data structure, so… resolve your way down the chain on the path towards programming excellence!