completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Stack frames & calling conventions

Learn how V8 uses stack frames for JavaScript calls, what arguments and locals live there, and how optimized and inlined frames stay debuggable.

By the end, you can
  • 01
    Draw calls as framesShow how the caller waits, the called function gets its own frame, and the caller goes on from the line saved in the return address.
  • 02
    Name the main slotsTell apart the return address, the arguments, the local variables, the context, and the slots that fast code uses.
  • 03
    Tell language rules from engine choicesKnow which facts are JavaScript rules, like missing and extra arguments, and which are V8's own choices, like bytecode, stack size, and copied-in functions.

Each call gets its own record

When one function calls another, the first function stops and waits. The function that makes the call is the caller. The function being called is the callee.

The engine is the program that runs your JavaScript. In Chrome and Node, the engine is called V8. While the caller waits, the engine must remember a few things:

  • where to go back to in the caller,
  • the values passed in, called the arguments,
  • the variables made inside the call, called the local variables,
  • the outer variables the function can still use.
The main idea

A stack frame is the record for one function call that is still running. It holds where to go back to, the arguments, the local variables, and links to other things the engine needs. When the call ends, its frame is thrown away.

You met the simple version of this in Call stack, Execution context, and Stack and heap. This lesson keeps that simple picture and adds real facts about V8. The last lesson, Heap layout, showed where long-lived values are kept. This lesson is about the short-lived records made while calls run.

Real-life analogyA stack of plates in your kitchen

Picture a pile of plates in your kitchen. You put a plate on top when you need it. When you are done, you clear the top plate first. Function calls work the same way: the last call to start is the first to end.

In real life: You put a plate on top when you need it
In JavaScript: A frame goes on top when a function starts
In real life: A note on the plate says where it goes back
In JavaScript: The return address says where the caller goes on
In real life: The food on the plate is only for this meal
In JavaScript: Local variables belong to this one call
In real life: You clear the top plate first
In JavaScript: The newest call ends first

You don't need to learn every byte of V8's frames. After this lesson you can:

  • read a stack trace, the list of calls printed with an error,
  • explain what happens with missing and extra arguments,
  • explain why fast, optimized code can still be debugged,
  • see where the next lesson, on stack overflow, starts.

The machine stack and the JavaScript stack

There are two stacks to keep apart:

  • The machine stack is a block of the computer's memory. V8's own code and the fast code it makes for your functions use it.
  • The JavaScript stack is the list of your JavaScript calls that are still running. You see it in DevTools and in an error's stack text.

In V8 the two are linked, but they are not the same thing.

In Node, V8 keeps JavaScript calls on the machine stack, and that stack has a fixed size. Node's --stack-size flag sets the size. A flag is an option you type after node on the command line.

The code below has a function that calls itself again and again. This is called recursion. It counts the calls until the engine runs out of room and throws a RangeError.

Node only: a bigger stack fits more callsJavaScript
let depth = 0;function dive() {  depth += 1;  dive();} try {  dive();} catch (error) {  console.log(error.name, depth);}

Line 1 starts a counter at 0. Line 3 adds 1 on every call. Line 4 calls dive again, so it never stops on its own. Line 8 makes the first call. Line 9 catches the error, and line 10 prints its name and the count.

What the test checks

The lesson's test runs this code twice: once with a small stack size and once with a bigger one. The bigger size always reaches more calls before the RangeError. The exact count changes from computer to computer, so the test doesn't check it. The next lesson is all about stack limits.

Four words in this lesson sound alike. The table says what each one means and what it keeps. Read each row from left to right.

Four words people mix up: two stacks, the heap, and a stack trace
WordWhat it meansWhat it keeps
Machine stackA block of the computer's memory that grows and shrinks as code calls functions.The real frames for V8's own code and for your JavaScript calls.
JavaScript stackThe list of your JavaScript calls that are still running.What you see in Error().stack, in DevTools, and in this lesson's pictures.
HeapMemory for things that can last longer than one call. The garbage collector cleans it up.Objects, arrays, strings, a closure's outer variables, and many of V8's notes.
Stack traceText that lists the calls at one moment, built from frames.A view for debugging. It is not a copy of memory.

What a stack frame holds

Let's follow a small checkout. The cart costs ₹150. The shop adds 18% GST, a tax on the price, so the total is ₹177. The code has three functions, and each one calls the next: checkout calls total, and total calls addTax.

Step through checkout, total, and addTax
Step 0 of 14Ready
Your turn: follow the blue line

Step through three calls in a simple model. It shows when each frame is added, which slots matter, and when each frame is removed.

Running in
  1. script
Next: line 20
function addTax(amount) {  const rate = 0.18;  const tax = amount * rate;  return amount + tax;} function total(items) {  let sum = 0;  for (const item of items) {    sum += item.price;  }  return addTax(sum);} function checkout(cart) {  const finalAmount = total(cart);  return `Pay ₹${finalAmount.toFixed(2)}`;} 
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

This step-through is a simple model. It is not a view of V8's real memory. Line 20 calls checkout. Line 16 calls total. Line 12 calls addTax. Each call gets its own frame. When a call returns, its frame is removed, and the line that was waiting goes on.

The frames while addTax runs. From top to bottom: addTax(150) returns to line 12. total(items) returns to line 16. checkout(cart) returns to line 20. The script frame is at the bottom.top of the stackaddTax(150)returns to line 12total(items)returns to line 16checkout(cart)returns to line 20scriptthe script itself
Each call adds a frame on top. Each frame remembers the line in its caller to go back to. This is its return address.

A frame has several parts, called slots. One slot, the context, points to where the function's outer variables are kept. A closure, a function that still uses variables from an outer function, needs it. In the table, each row names a slot, says what it is for, and says where it is kept.

The slots of a stack frame
SlotWhat it is forWhere it is kept
Return addressIt says where the caller goes on after the call ends.In the machine stack, as a place in the caller's code.
Link to the caller's frameIt points back to the caller's frame. Tools follow these links to list every call.In the frame header, the fixed slots at the start of the frame.
ContextIt points to the outer variables the function can use, like the ones a closure keeps.The link is in the frame. The outer variables usually live in the heap.
Function detailsIt says which function is running, its bytecode, and how far it has got.In the frame header and in V8's notes about the code.
Argument countIt says how many values the caller really passed.In the frame of the called function. V8 has kept it there since it removed the arguments adaptor frame.
Arguments and thisThey are the values passed in, plus the object that becomes this.Where the caller's frame meets the called function's frame, as the calling convention says.
Local valuesThey are the call's own variables and short-lived results.In the interpreter's registers, or in spill slots in fast code.

A frame doesn't hold big things like arrays. In total, the frame keeps the number sum. For items, it keeps only a link to the cart array. The array itself lives in the heap, the memory area for objects that can last longer than one call.

The frame for total holds two things: sum, which is 150, and items. An arrow goes from items to the cart array, which sits outside the frame in the heap.frame for totalsum: 150itemstotalpoints toheap[notebook, pen]the cart array
The frame for total keeps the number sum itself. For items it keeps only an arrow to the cart array. The array lives outside the frame, in the heap.
Real-life analogyA contact card on your phone

On your phone, you open a contact card. The card shows a few details and links to more, like a saved address. While it is open, you might type a quick note. When you tap Back, you go to the screen you were on before. A frame works the same way.

In real life: The contact card you have open now
In JavaScript: The frame of the call that is running
In real life: The card links to a saved address
In JavaScript: The context slot points to outer variables
In real life: A quick note you type while it is open
In JavaScript: The local variables of this call
In real life: The Back button takes you to the last screen
In JavaScript: The return address goes back to the caller

How V8 starts a function call

A calling convention is a fixed plan for where each piece of a call goes. The plan covers:

  • the object that becomes this, called the receiver,
  • the arguments,
  • the return address,
  • a link to the caller's frame,
  • a few fixed slots at the start of the frame, called the frame header.

V8 changes this plan between versions. What your JavaScript sees must stay the same.

The V8 article "Faster JavaScript calls" explains two changes. First, V8 now puts the arguments on the stack in reverse order. Second, it removed an extra helper frame, the arguments adaptor frame. That frame used to fix up calls with too few or too many arguments.

Now the frame of the called function keeps the real number of arguments. With that number, V8 can still build arguments and rest parameters, and it can clean up after the call.

V8 first turns each function into bytecode: short, simple instructions that it runs one at a time. The code below asks Node to print the bytecode for addTax. It works in Node only, not in the browser.

Node only: print the bytecode for addTaxJavaScript
function addTax(amount) {  const rate = 0.18;  return amount + amount * rate;} addTax(150);
What the test checks

The test runs this in Node 22, which has V8 12.4, with the flags --print-bytecode --print-bytecode-filter=addTax. It checks facts that stay the same on every run:

  • V8 prints bytecode for addTax.
  • The bytecode shows a parameter count.
  • The math steps use a0, which is the first argument, amount.
  • There is a Return step.

It doesn't check memory addresses or exact positions, because those change.

Too few or too many arguments

In JavaScript you can pass fewer or more arguments than a function names. This is a rule of the language, not a V8 trick:

  • A missing parameter gets the value undefined.
  • An extra value has no name, but you can still read it. Every normal function has arguments, a list-like object with every value passed in, in order.
Step through too few and too many arguments
Step 0 of 9Ready
Your turn: follow the blue line

Compare a call with too few values and a call with too many. The frame slots are a simple model; the printed results come from real JavaScript.

Running in
  1. script
Next: line 1
  const passengerName = passenger ?? "Guest";  const destination = city ?? "Jaipur";  const extra = arguments[2] ?? "no extra";  return `${passengerName} -> ${destination}; extra: ${extra}; supplied: ${arguments.length}`;} console.log(makeBooking("Asha"));console.log(makeBooking("Ravi", "Kochi", "window seat"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Left: line 8 passes one value. passenger is "Asha" and city is undefined. arguments.length is 1. Right: line 9 passes three values. passenger is "Ravi", city is "Kochi", and arguments[2] is "window seat". arguments.length is 3.line 8: one valuepassenger = "Asha"city = undefinedarguments.length = 1line 9: three valuespassenger = "Ravi"city = "Kochi"arguments[2] = "window seat"arguments.length = 3
The function always has its two named parameters. A missing value becomes undefined. An extra value has no name, but arguments[2] still holds it.
Playground: pass one, two, or four values
A function that counts its parameters and the values passedJavaScript
function receiptLine(item, quantity) {  return {    declared: receiptLine.length,    supplied: arguments.length,    item,    quantity,    extras: Array.from(arguments).slice(2),  };} receiptLine("chai", 2, "GST", "takeaway");
Frame model1 argument
declared2

receiptLine.length counts the named parameters.

supplied1

arguments.length counts the values this call passed.

itemchai

The first named parameter.

quantityundefined

The second named parameter.

return address -> back to the playground

argument count -> 1

item -> chai

quantity -> undefined

extras -> none

Extra values

This call passed no extra values.

Step 1 of 41 argument
Call receiptLine with:

This call passed 1 value. The function names 2 parameters, so quantity is undefined. Extra values: none.

This is a simple model. It runs a real JavaScript function, then lists the slots a frame would need. It doesn't look inside your browser's memory.

Now check the same facts with plain code. inspectCall.length is the number of parameters the function names. arguments.length is the number of values this call passed. The first call leaves second as undefined. The second call passes three values, so "window seat" is in arguments[2].

Named parameters and passed values, countedPop out in the code editor (opens in a new tab)JavaScript
function inspectCall(first, second) {  console.log("declared", inspectCall.length);  console.log("supplied", arguments.length);  console.log("first", first);  console.log("second", second);  console.log("extra", arguments[2]);} inspectCall("Asha");inspectCall("Ravi", "Kochi", "window seat");

A rest parameter, written ...rest, collects the extra values into an array. A function's length stops counting at the rest parameter, so collect.length is 1.

A rest parameter and the function's lengthPop out in the code editor (opens in a new tab)JavaScript
function collect(first, ...rest) {  console.log(collect.length);  console.log(first);  console.log(rest.join("|"));} collect("chai", "samosa", "jalebi");

Simple frames and fast frames

V8 first runs your code with Ignition, its interpreter. An interpreter reads bytecode and does one step at a time. Its frames always have the same shape:

  • a header with the context, the function, its bytecode, the current position in the bytecode, and the argument count,
  • then a row of registers: small slots that hold the function's local values.

When a function runs many times, V8 may compile it with TurboFan, its optimizing compiler. TurboFan makes fast machine code. It chooses where each value goes. Some values stay inside the processor. Others are saved in frame slots called spill slots.

Fast code makes guesses about your values. If a guess turns out wrong, V8 switches back to the slow, safe version. This is called deoptimizing. To do it, TurboFan keeps notes on how to rebuild every JavaScript value. The same notes help V8 build stack traces.

V8 has several kinds of frames. Each row of the table says what one kind holds and why it exists.

The kinds of frames in V8
Kind of frameWhat it holdsWhy it exists
Ignition frame (the interpreter)A header with the function, its bytecode, the position in it, and the argument count, then registers.It starts fast and watches what values your code uses.
Sparkplug frame (the baseline compiler)Machine code with a simple layout that still matches the bytecode closely.It runs faster than the interpreter and is cheap to make.
TurboFan frame (the optimizing compiler)Slots the compiler picks, plus notes for switching back to slow code.It is the fastest code, but V8 must still be able to rebuild every JavaScript value.
Copied-in (inlined) frameNo frame of its own while the code runs. V8's notes remember the call.V8 can rebuild it from the notes, so it still shows in stack traces.
An engine choice, not a language rule

The JavaScript spec, the official rule book, says how calls, parameters, scopes, and return values behave. It doesn't say how a frame is laid out. V8's frame header, Ignition registers, and TurboFan spill slots are V8's own choices. Other engines can do it differently.

Copied-in functions still show in stack traces

Inlining means the optimizing compiler copies a small function's code into the function that calls it. Then the machine doesn't need a separate call, so the copied function may get no frame of its own.

V8 still keeps notes about the copy. When it builds a stack trace, it uses the notes to list the copied function as if it had been called.

The code below works in Node only. The lines that start with % are special V8 commands. They work only with the --allow-natives-syntax flag. Here they make V8 optimize checkout and report whether it is optimized.

Node only: an optimized, copied-in function still shows in the stackJavaScript
function gstLine(amount) {  return captureStack(amount * 1.18);} function captureStack(total) {  return new Error("receipt " + total).stack;} function checkout(amount) {  return gstLine(amount);} %PrepareFunctionForOptimization(gstLine);%PrepareFunctionForOptimization(captureStack);%PrepareFunctionForOptimization(checkout);for (let i = 0; i < 10000; i += 1) checkout(i);%OptimizeFunctionOnNextCall(checkout);const stack = checkout(100);const status = %GetOptimizationStatus(checkout);console.log("status", status);console.log("optimized", Boolean(status & (1 << 4)));console.log(stack.split("\n").filter((line) => /captureStack|gstLine|checkout/.test(line)).join("\n"));
Left: the fast version of checkout has the gstLine code copied inside it. Right: the error.stack text still lists at captureStack, at gstLine, and at checkout.fast checkout codecheckoutgstLinegstLine copied insideerror.stackat captureStackat gstLineat checkoutall three names listed
The fast version of checkout has the gstLine code copied inside it, so there is no separate gstLine call. The stack trace still lists gstLine, because V8 keeps notes to rebuild it.
What the test checks

The test runs this with --allow-natives-syntax --no-maglev --trace-turbo-inlining. It checks that:

  • Node is version 22, with V8 12.4.
  • V8 reports that gstLine was copied into checkout.
  • checkout is optimized.
  • The stack text still names captureStack, gstLine, and checkout.

V8 also limits how many lines a stack trace shows. A stack trace is text made for debugging. It is not a copy of memory.

The code below makes three nested calls and counts the at lines in the trace. With Node's --stack-trace-limit=2 flag, it prints frames 2. A later lesson on the stack trace API goes further.

Node only: a limit on stack trace linesJavaScript
function leaf() {  return new Error("limit").stack;}function middle() {  return leaf();}function top() {  return middle();} const frames = top().split("\n").filter((line) => line.trim().startsWith("at "));console.log("frames", frames.length);

Using frames when you debug

You will use this most when you debug. When an error points at a helper function, read the top line of the stack trace first. Then go down to the functions that called it. At each step, check what was passed in:

  • too few arguments,
  • too many arguments,
  • an object that some other code changed.
A receipt helper you might debugPop out in the code editor (opens in a new tab)JavaScript
function formatReceipt(items, coupon) {  const subtotal = items.reduce((sum, item) => sum + item.price, 0);  const discount = coupon ? subtotal * coupon.percent : 0;  return subtotal - discount;} console.log(formatReceipt([{ price: 500 }, { price: 250 }], { percent: 0.1 }));console.log(formatReceipt([{ price: 500 }, { price: 250 }], null));

Line 2 adds up the prices and keeps the result in the local variable subtotal. Line 3 takes off a discount only if a coupon was passed. Line 7 passes a 10% coupon and prints 675. Line 8 passes null and prints 750.

  • Read a stack trace as a list of your JavaScript calls, not as raw machine memory.
  • Use arguments.length or a rest parameter when a function really needs to know how many values were passed.
  • Remember that a local variable can point to an object in the heap, and that object can outlive the frame.
  • Measure before you change code because of a guess about frames. Many arguments alone don't make code slow.

Common misconceptions

  • "The stack trace is the stack." A stack trace is text that V8 builds from frames and its notes.
  • "A frame holds every value its function uses." Objects live in the heap. A frame can point to them, and they can last longer than the frame.
  • "fn.length tells me how many arguments were passed." It counts the parameters the function names. arguments.length counts the values passed in this call.
  • "Optimized code can't be debugged." V8 keeps notes so tools can rebuild the JavaScript values and calls.
  • "V8's frame layout is part of JavaScript." The language rules are written in the spec. The frame layout is V8's own choice.

Some pairs of ideas are easy to mix up. Each row says what one idea means and what it is not.

Ideas that are easy to mix up
IdeaWhat it meansWhat it is not
fn.lengthHow many parameters the function names, up to the first default value or rest parameter.It is not how many values this call passed.
arguments.lengthHow many values this call really passed.It is not part of the function's code. It can change on every call.
A stack frameThe record for one running call, plus links and details.It is not an object in the heap, and it is not a block of your code.
A copied-in (inlined) frameA call that V8 rebuilds from its notes for a stack trace.It may not have a real frame of its own in the machine stack.
Frame, heap, or engine notes?
  • The return address that lets checkout go on after total(cart).
  • The number in sum while total is running.
  • The cart array and the item objects in it.
  • Outer variables that a closure still uses after the outer call has ended.
  • The bytecode for addTax.
  • Notes that let fast code rebuild copied-in calls for a stack trace.
Try it yourself
0 of 6 correct

Put each card where it mainly lives in this lesson's picture: in a stack frame, in the heap, or in V8's notes about your code.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Practice exercises

Exercise 1 · Warm-upPredict the two counts

Type the three printed values in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function inspectCall(first, second) {
  console.log(inspectCall.length);
  console.log(arguments.length);
  console.log(second);
}
inspectCall("Asha");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the extra argument

    What value does the last console.log print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function route(passenger, city) {
      console.log(route.length);
      console.log(arguments.length);
      console.log(arguments[2]);
    }
    route("Ravi", "Kochi", "window seat");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeDraw the two top frames

      On paper or in code comments, draw the frames at the moment total(cart) adds the second item in the checkout example.

        Exercise 4 · PracticeName the slot that goes back

        Which frame slot says where the caller goes on after the call ends?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix a wrong argument count check

          The starter code tries to spot missing arguments. But percent = 0 gives percent a default value, and that makes the check wrong. Fix it so the call prints 1000.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function priceWithDiscount(price, percent = 0) {
            if (arguments.length < priceWithDiscount.length) return "missing";
            return price - price * percent;
          }
          console.log(priceWithDiscount(1000));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeUse it on a real stack trace

            Open a real project or a recent bug report. Next to each of the top three lines of a stack trace, write the argument or object that links it to the next one.

              Check your understanding

              Stack frames quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What is a stack frame?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function inspect(first, second) {
                  console.log(inspect.length);
                  console.log(arguments.length);
                  console.log(second);
                }
                inspect("Asha");

                Choose an answer to see the explanation.

              3. Question 3 of 7V8 removed the arguments adaptor frame. What does the frame of the called function keep now?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does this code print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function collect(first, ...rest) {
                  console.log(collect.length);
                  console.log(rest.length);
                }
                collect("chai", "samosa", "jalebi");

                Choose an answer to see the explanation.

              5. Question 5 of 7Which thing usually lives in the heap, not in one frame?

                Choose an answer to see the explanation.

              6. Question 6 of 7V8 copied a small function into its caller. What can happen to it in a stack trace?

                Choose an answer to see the explanation.

              7. Question 7 of 7A stack trace surprises you. What is the best habit?

                Choose an answer to see the explanation.

              Key takeaways

              • A stack frame is the record for one running call: where to go back to, the arguments, the local variables, and links.
              • The machine stack, the JavaScript stack, the heap, and a stack trace are linked, but they are four different things.
              • Ignition frames always have the same shape. TurboFan frames use slots the compiler picks, plus notes for going back and for debugging.
              • A missing parameter is undefined. An extra value is still in arguments. Modern V8 no longer needs an arguments adaptor frame.
              • A function copied into its caller can still show up in a V8 stack trace.

              In one line: a function call is more than a jump. It makes a short-lived record that lets the engine go back, help you debug, and keep JavaScript's rules.

              Next, Stack limits & stack overflow shows what happens when too many frames pile up, and how to rewrite risky recursion.

              CompleteFrontend Clear concepts. Working examples.