Read Stack Traces Effectively — Debugging Systematically, Part 3
- Contributor
- 10 hours ago
- 3 min read
Debugging Systematically · Part 3
A stack trace is a story: how the program got here. Read it deliberately and you'll find the bug fast.
Step 1: Direction Matters (5 min)
Python (and most languages):
Traceback (most recent call last):
File "main.py", line 10, in <module>
process(data)
File "processor.py", line 25, in process
validate(data)
File "validator.py", line 8, in validate
raise ValueError("Invalid")
ValueError: Invalid
Top: the entry. Bottom: where it failed.
Different conventions in different languages. JavaScript prints the failure first.
Find the actual error first; trace back to your code.
Step 2: Find Your Code (5 min)
A trace often has 100+ frames; most are framework internals.
Skim from the failure upward. Look for your file paths. That's where to focus.
... 30 frames in fastapi/starlette/uvicorn ...
File "src/myapp/handlers/orders.py", line 42, in create_order ← your code
... 20 frames in pydantic ...
Skip framework noise. Find the application code.
Step 3: Read the Error Message (5 min)
KeyError: 'user_id'
AttributeError: 'NoneType' object has no attribute 'name'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
The message often tells you exactly what to fix. Read it before guessing.
NoneType has no attribute X = the thing you thought was an object is None.
Step 4: Inspect the Variables (10 min)
Most stack traces just give you locations. Sometimes you can see local values:
Python: traceback.print_exc() with debugger; --locals in pytest. Sentry: shows local variables at each frame. Some loggers: include frame state.
The values often explain the failure.
Step 5: Tail of the Trace (10 min)
In long-running services, the "real" error is often at the bottom and the propagation hides above it:
... 100 frames of "RetriedException" ...
... 50 frames of "FastAPIException" ...
... actual error 200 lines down: KeyError ...
In wrapped exceptions, look for caused by: or from:
try:
...
except Exception as e:
raise BadStateException("Failed to load") from e
from e chains. The trace shows both; the original cause is in the chain.
Step 6: Async Stack Traces (10 min)
Async traces are confusing — coroutines, gather, task boundaries.
In Python 3.11+:
RuntimeError
File "main.py", line 10, in run_main
await process_all()
File "main.py", line 5, in process_all
await asyncio.gather(*tasks)
+ Exception Group Traceback (most recent call last):
| RuntimeError: failed
+ ...
Exception groups (TaskGroup, gather) show all failures. Read each branch.
For older Python: pip install asyncio-extras or use try/except around each task.
Step 7: Look for Patterns (5 min)
Repeated frames = recursion (likely infinite). Many threads = concurrency issue. Specific function appearing across reports = focus area.
If you see a trace 100 times in production, the function at the top of "your code" portion is where to look first.
Step 8: Reproduce in Isolation (10 min)
A complex trace is easier to debug if you can reproduce in isolation:
def reproduce():
# Just the call that fails
process(data_from_logs)
reproduce()
Smaller scope; faster iteration; clearer trace.
Step 9: Add Context to Exceptions (10 min)
Bare exceptions are hard to debug:
# Bad
raise ValueError("Invalid")
# Better
raise ValueError(f"Invalid user_id={user_id}, status={status}")
When the exception fires in production with 1000 users, you know which one.
Step 10: Log Stack Traces (5 min)
import logging
logger = logging.getLogger(__name__)
try:
do_work()
except Exception:
logger.exception("Failed to do work")
raise
logger.exception includes the stack trace. Critical for post-mortem of issues you didn't catch live.
What You Just Did
Stack traces: direction, finding your code, error messages, locals, exception chains, async, patterns, reproduction, context. Trace-reading as a skill.
Common Failure Modes
Read only the top. Miss the chained "caused by."
Skim past the error message. "KeyError: foo" tells you the bug; you didn't read it.
Mock without context. "Test passes" doesn't help if the trace is from production.
No exception chaining. Lose original cause when re-raising.
Bare except: clauses. Swallow exceptions; lose traces.
Continue the Debugging Systematically path
Previous — Part 2: Bisect to Find the Cause
Part of the Debugging Systematically learning path.


