Transcript
  • JavaScript

The Call Stack

From the class:  The JavaScript Runtime

Oftentimes, as developers, the only real interaction we have with the call stack is when an error gets thrown, like this one in the console. Normally, we get some kind of cryptic error message, along with a line number for the file where the error actually got thrown. And if we drill into the error, you see something called a stack trace. And normally what so we do with that, if you're anything like me, is you look through the stack trace and try to find something that you recognize that might have caused the problem. And then, you can click into it and see the specific function that threw the error.

But the call stack is a really fundamental part of the programming environment. And so understanding it intuitively will make you a lot better at programming. So in the rest of this video, I'd like to take a look at what the call stack does, and how we can work with it in the debugger.

I'm going to start by getting rid of this code that I had here to begin with. And we'll just put a debugger statement so that we get dropped into the debugger in Chrome. And then right below that, I will a print out to the console log that we're running our program. And over on the debugger, I'll refresh this.

And right away, we get dropped into the debug statement here. And what I want to draw your attention to is the call stack area over on the right. Notice it has one entry here called Anonymous Function at app.js line 1. This is the first entry in the call stack. And it's the main part of our program that's executing here.

We haven't called any other functions yet. And so we can step through the program. And then, once it's done, notice the call stack is just empty. So there's nothing else in the call stack. Nothing is executing at the moment.

Next, let's look at what happens when we call into a function. So what I'll do is define a function here called 1. And we'll print to the console 1. And then, I'll just return the number 1. It's not a very useful function. But it'll demonstrate the concept. And down here, I'll call that function.

And then, over in the debugger, we'll refresh this. And we get dropped into the debugger again. And notice in the call stack, right now, we only have one entry.

Now our function, 1, has been defined. But we haven't called it yet. So you can see that it's been defined, because in the console down below, I can type 1, and there's a value for that identifier.

Now, as we step into the function, notice that our call stack has grown by one entry. So we have the Anonymous function that we started with that was created automatically. And then we called in to Function 1. And now that is on the call stack. So the call stack now has two entries.

Additionally, the call stack in the debugger is showing us that we're currently on line 5 in the application, or app.js file.

Let's add another function to the stack. Over in the code, right above Function 1, I'll declare another function called Function 2. And similar to Function 1, we'll print to the console 2. And then, we'll return the number 2.

And in Function 1, before we return, let's create a variable for the return value. And we'll call function 2.

All pretty basic, but it's helping us to visualize what's happening in the debugger. So when I refresh this, there we are, back in the beginning of the program. And we'll step into Function 1. And then again, we'll step into Function 2.

Now, notice we have another entry in the call stack for the Function 2. So we now have three functions in the call stack. So the stack is growing here from the bottom up.

And each time we call another function from within a function, like we have here, the call stack grows by one. And so in a regular application, our call stack could get quite large-- even into 100 function calls, for example.

Now, notice what happens as we continue to step through the program. Function 2 is going to return. And we're going to end up right back in Function 1. And notice that the call stack has been decremented by 1. In other words, the value of the 2 entry up here was popped off the stack. And now we're back at Function 1 on line 12.

And similarly, if we finish executing Function 1, we end up back at the beginning, the Anonymous function. And then, when the program is finished executing, the call stack goes back to being empty.

Now, one of the errors you've probably seen at least once in your programming career is the Stack Overflow exception. Let me show you what that looks like.

In the console, or in the code, I'm going to get rid of this debug statement, and instead, put it right in here. And we'll get rid of this. And inside of this Function 1, I'm going to recursively call the Function 1 again. So we'll just keep calling into the Function 1 over and over again. And I'll put a debugger statement here.

So over in the console, when I refresh, look over in the call stack. So so far, we have 1 in the call stack in our Anonymous function. If I step through here and just press Play, now we have 1 on the call stack twice. That's because we've called in to the Function 1 two times. If I press Play again, get another one. And it just grows indefinitely, until eventually we'll run out of memory, and we'll get an error that says that we've had a Stack Overflow. So we've run out of memory for the stack to use.

But what does a stack really mean? I'd like to get a little bit more intuition about what it looks like and why we use it. Why not just use another kind of data structure?

So remember that we're studying the runtime, which is the implementation, or the code that runs our code. And so if we hop over to Wikipedia and take a look at the history of the stack, you can see that initially, according to Wikipedia, the first stack was proposed in 1946 by Alan Turing. And the purpose of it was a way to implement calling and returning from subroutines or functions.

And so you can imagine yourself as someone who's writing the runtime, or writing the code that's going to run our code. And now, you have to solve this problem of how do you call functions and then return from functions? And how do you give programmers at the higher level the ability to do this easily?

So one way we can help to understand this more is to go over and implement a very, very simple version of this in JavaScript, since we already know that. So in our application JS file, the purpose here is just to start to think a little bit about what an implementation might look like. It's not to make this perfect. It's just to get some better intuition.

So let's just pretend that we have something called a heap. And that's where we're going to put just random things that we want in memory to be able to access them, like our function. And let's create another data structure called a stack. And that'll just be an array. And we'll push and pop elements on and off of the array.

Then, we'll create a function called Call Function. And that function might take the name of the function, any arguments we want to pass to it, maybe the line number that we're currently on.

And inside of this-- and remember, this would normally be written in C++ or in a lower-level language that's being used to run our code-- we'll create something called a stack frame, which will look like just a regular object. And on that, we might say that the function body is where the function itself comes from the heap. So find us the function body there. And since we're going to call it in a second, I'm just going to go ahead and up here call it a function.

And then, on the stack frame object, we can put some things that we can make available to the function itself-- for example, the arguments, and then, maybe, the environment for looking up variables. And we might put the return value as well. And since it hasn't returned yet, we'll just start this off as undefined.

Then, we can go ahead and push this stack frame onto the stack, and call the function. And maybe we'll call the function with the arguments that were passed in. We want to grab the return value. And then, we can put that on the stack frame as well.

And then, finally, when our function returns, we can pop it off of the stack. So I'll say stack dot pop. And that will grab the last stack frame on the stack and return it. And you can see how this object here can be used to communicate between a caller and a callee. For example, the caller might get the return value from this object.

So this is a bit of a contrived example, but I'm hoping that it helps you to start to visualize a bit what the runtime has to do. So hopefully, this video gave you a better understanding of how a stack can be used to implement calling and returning from subroutines or from functions.