top of page

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

Part of the Debugging Systematically learning path.

bottom of page