Stack Traces
Following the Breadcrumb Trail
Imagine you're exploring a maze. At every turn, you drop a breadcrumb so you can trace your path back to the entrance. If you get stuck, you can look at the trail of breadcrumbs and see exactly how you got there.
Stack traces work the same way. When your program calls a function, the computer drops a "breadcrumb" -- it remembers where the call happened. If something goes wrong deep inside a chain of function calls, those breadcrumbs tell you the full path the program took to reach the error.
What a Stack Trace Looks Like
Here's a program with a bug hiding inside nested function calls:
When you run this, Pebble shows:
Traceback (most recent call last):
line 9, in <main>
line 6, in calculate
Error: Division by zero at line 2, column 12
Reading from top to bottom:
- line 9, in
<main>-- the program started here, callingcalculate(). - line 6, in
calculate-- insidecalculate, it calleddivide(10, 0). - Error -- inside
divide, division by zero happened at line 2.
The trace reads like a story: "I was on line 9 of the main program,
which called calculate on line 6, which hit a division-by-zero error
on line 2."
Why Stack Traces Matter
Without a stack trace, you'd only see:
That tells you what went wrong and where the error happened, but not
how the program got there. If divide is called from ten different
places, you wouldn't know which call caused the problem.
The stack trace answers the question: "What was the program doing when things went wrong?"
Deeper Call Chains
The more functions involved, the longer the trace:
fn step_one() {
let x = 1 / 0
}
fn step_two() {
step_one()
}
fn step_three() {
step_two()
}
step_three()
Traceback (most recent call last):
line 13, in <main>
line 10, in step_three
line 6, in step_two
Error: Division by zero at line 2, column 13
Each line in the trace is one "breadcrumb" -- one function call that led to the next, all the way down to the error.
Try/Catch and Stack Traces
When you catch an error with try/catch, the stack trace doesn't
appear -- because the error was handled. The program keeps running:
Output: Caught: Division by zero
No stack trace, because the catch block dealt with the problem.
Stack traces only appear for uncaught errors -- ones that nobody
handled.
Using the Debugger
The debugger's bt (backtrace) command shows you the call stack
while the program is running, with line numbers for each frame:
The > arrow marks the frame you're currently in. This is handy for
understanding where you are when stepping through code.
How It Works Under the Hood
Every time Pebble calls a function, the VM pushes a frame onto the call stack. Each frame remembers:
- Which function it belongs to (the function name).
- Which instruction it was about to execute (the instruction pointer).
- The local variables for that call.
When an error happens, the VM walks the call stack from bottom to top. For each frame, it looks at the instruction that made the call and reads its source location (line and column). It packs all of these into a list of TraceEntry objects and attaches them to the error.
The format_traceback function then turns that list into the
human-readable output you see on screen.
graph TD
A["main calls calculate() on line 9"] --> B["calculate calls divide() on line 6"]
B --> C["divide hits error on line 2"]
C --> D["VM walks frames: main -> calculate"]
D --> E["Builds TraceEntry list"]
E --> F["format_traceback renders output"]
Summary
| Concept | What It Means |
|---|---|
| Stack trace | The chain of function calls that led to an error |
| Frame | One "breadcrumb" -- a function call with its line number |
<main> |
The top-level program (not inside any function) |
| Traceback | Pebble's name for the stack trace in the error output |
bt command |
Debugger command to see the call stack while running |