Callstack and why it's important to understand in development
๐๐ง ๐ข ๐ฑ๐ณ๐ฐ๐จ๐ณ๐ข๐ฎ๐ฎ๐ฆ๐ณ ๐ฅ๐ฆ๐ด๐ช๐จ๐ฏ๐ด ๐ข ๐ฑ๐ณ๐ฐ๐จ๐ณ๐ข๐ฎ, ๐ฐ๐ฏ๐ญ๐บ ๐ฉ๐ข๐ญ๐ง ๐ต๐ฉ๐ฆ ๐ซ๐ฐ๐ฃ ๐ช๐ด ๐ฅ๐ฐ๐ฏ๐ฆ ๐ช๐ง ๐ต๐ฉ๐ฆ๐บ ๐ฉ๐ข๐ท๐ฆ ๐ฐ๐ฏ๐ญ๐บ ๐ฅ๐ฆ๐ด๐ช๐จ๐ฏ๐ฆ๐ฅ ๐ต๐ฉ๐ฆ ๐ฅ๐ข๐ต๐ข ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ถ๐ณ๐ฆ๐ด. -- ๐๐ฉ๐ฆ๐ฏ๐จ, ๐๐ฐ๐ฏ๐ฆ๐ด, & ๐๐ฉ๐ช๐ฎ๐ฃ๐ญ๐ฆ๐ฃ๐บ
For this article I would like to shed light on a complex side in programming, and that is understanding the callstack.
I'll go over the JavaScript callstack as it's one of the most popular languages in todays programming ecosystem.
The callstack may seem a bit daunting (maybe like the godzilla gif below) "Everybody runnnn for your lives!!" But if you make it through this article, my hope is that it will provide you with better insight on the topic and give you a general idea of how the callstack operates.
I don't believe it's discussion is popularized in modern web development because it has many layers and is quite deep in theory. The word we usually use for that is "abstraction" - where you take something complicated and put a nice interface around it so that you don't have to deal with the complexities.
The callstack is a very low level concept, It goes deep in showing you all the layers of your program - including the framework code. That's why I believe understanding it will be of great use as you can pick apart where your code is going and what's happening to it.
I'll go over various topics such as:
How functions enter and exit the callstack and what happens if they never exit the callstack
How and why I believe the callstack is like a washing machine.
Why the callstack is an absolute necessity in coding
How the code we provide to our callstack (washing machine) can cause the callstack to not work as we expect
For our first example we'll start with an extremely basic recursive function:
(๐ง๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ ๐ญ๐ข๐ถ๐ฏ๐ฅ๐ณ๐บ() {
๐ญ๐ข๐ถ๐ฏ๐ฅ๐ณ๐บ()
})();
- ๐ฉ๐ฆ๐ณ๐ฆ ๐ธ๐ฆ ๐ข๐ณ๐ฆ ๐ค๐ข๐ญ๐ญ๐ช๐ฏ๐จ ๐ต๐ฉ๐ฆ ๐ญ๐ข๐ถ๐ฏ๐ฅ๐ณ๐บ ๐ง๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ ๐ช๐ฏ๐ด๐ช๐ฅ๐ฆ ๐ช๐ต๐ด๐ฆ๐ญ๐ง ๐ธ๐ช๐ต๐ฉ๐ฐ๐ถ๐ต ๐ข ๐ด๐ต๐ฐ๐ฑ๐ฑ๐ช๐ฏ๐จ ๐ค๐ฐ๐ฏ๐ฅ๐ช๐ต๐ช๐ฐ๐ฏ ๐ฐ๐ณ "๐ฃ๐ข๐ด๐ฆ ๐ค๐ข๐ด๐ฆ"
As soon as you invoke this function it is sent to the callstack and creates what is known as a "stack frame" and will be temporaily stored until it has been returned.
If you visualize the callstack as a washing machine, we are bringing our laundry() function to the washing machine which is where it will be held until "washed" however, what would happen if we never took our laundry() out of the washing machine?
Great question! Notice anything odd about our function above? think about it for a minute. When you think you have it, scroll past Zach to review the answer
wฬถeฬถ ฬถnฬถeฬถvฬถeฬถrฬถ ฬถsฬถeฬถtฬถ ฬถaฬถ ฬถsฬถtฬถoฬถpฬถpฬถiฬถnฬถgฬถ ฬถcฬถoฬถnฬถdฬถiฬถtฬถiฬถoฬถnฬถ!
That's right! This is what's known as stack overflow and a common problem we'll all eventually run into in programming, we never took our clothes out of the washing machine, and you may aswell throw a brick into it:
You'll also end up with a nasty error like the one below:
Our laundry() function kept running itself until our callstack reached its maximum request limit, or basically exploded like the laundring machine above!
The callstack does alot for us in the background of our app, its where our code is processed. But we need to also give it some guidance, for example:
If you ordered a pizza but gave no directions and no specific details for your pizza to the restaurant, would you expect the pizza to show up on time and with the right toppings?
Of course not, and the callstack has no idea what functions we want processed first and when to process them, it's there to run our functions.
This is why understanding the callstack "hierarchy" is important. As developers we want to have precedence on what order our functions are processed.
My goal for this next example is to visualize why ordering our functions is important if we want the right result and how our functions are entering the callstack, and how they will be able to "exit" the callstack to prevent our washing machine from blowing up.
Lets analyze the code below:
On line 12 we invoke decisionB (aka execute the script). All the functions in this script are then sent to our callstack to be processed. If we look into decision B we can see that it relies on decisionA, therefore decision A will need to be processed aswell to return the data we're are relying on for decisionB. How we can make sure decisionA also makes it to the callstack is including it before we execute our script on line 12 this is because JavaScript performs synchronously, meaning, one at a time and "top to bottom". Because we need to use the result of decisionA as data to for our decisionB we need to make sure we invoke decisionA so we can have it added to the callstack.
This is an example of a successfull execution in the callstack and returns our desired message:
"๐๐ถ๐บ ๐จ๐ณ๐ข๐ฏ๐ฏ๐บ ๐ด๐ฎ๐ช๐ต๐ฉ ๐ข๐ฑ๐ฑ๐ญ๐ฆ๐ด, ๐ฃ๐ฆ๐ค๐ข๐ถ๐ด๐ฆ ๐ต๐ฉ๐ฆ๐บ ๐ข๐ณ๐ฆ ๐ต๐ฉ๐ฆ ๐ฃ๐ฆ๐ด๐ต";
So what will happen if we place our decisionA function after we send our script to the callstack from line 12? (example below)
We end up with this error because, our decision A is never added to our callstack and therefore cannot be processed by our callstack.
In our last example here we dived a bit deeper into theory. We should hopefully understand why our interface displaying the error "undefined" is not exactly the real reason for script not to run as we intended.
I hope after reading this, you've become even a bit more aquainted with what's happening behind the scenes when we invoke our functions.