top of page

Read Stack Traces Effectively — Debugging Systematically, Part 3

Shawn West
Jul 13
3 min read

Updated: Aug 6

Debugging Systematically · Part 3

A stack trace looks like a wall of intimidating noise, which is why beginners scroll past the part that actually tells them what happened. Read the right way, it's a precise map from the crash back to the line you need to fix. This walks through reading traces effectively: which direction to read, how to find your code among the frames, and how to turn the error message into a lead instead of a shrug.

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