Reading a Traceback Correctly
Imagine a traceback as a stack of dominoes falling. While the last domino to fall (the final exception) might be the one you see, the root cause is the very first domino that initiated the chain reaction. Python's chained tracebacks effectively show you all the dominoes in order.
The Setup
You are reviewing logs for a crashed microservice. An exception chain was raised, and you need to figure out which line actually triggered the original failure versus which line handled it.
What Does This Print?
def fetch_data_from_cache(key):
raise KeyError(f"Key '{key}' not found in cache")
def get_user_profile(user_id):
try:
fetch_data_from_cache(user_id)
except KeyError as e:
# Chaining a new exception
raise RuntimeError("Failed to resolve user profile") from e
# Executing the entry point
get_user_profile("user_12345")
The Output
Python's traceback outputs show the original exception at the top of the terminal output and the final raised exception at the very bottom. When an exception is caught and re-raised using raise ... from e, the entire historical chain is preserved in the terminal. If you only look at the bottom line, you will miss the root cause of the error, leading to misdirected debugging and wasted hours patching secondary symptoms. The stack trace lists active execution frames in chronological order, with the oldest events printed first. Understanding this structured hierarchy allows developers to locate exactly where the crash originated, rather than focus on where the exception propagation halted.
Why Python Does This
CPython attaches traceback objects to exceptions via the __traceback__ attribute during frame unwinding. When you raise an exception inside an except block, CPython automatically links the existing exception to the new exception's __context__ attribute. If you use the explicit raise ... from e syntax, it populates the __cause__ attribute. When printing the exception to stderr, the interpreter's default formatting logic recursively climbs the __cause__ or __context__ chain, outputting each traceback starting from the earliest exception. This ensures that the chain of causality is never lost during execution error handling.
The Fix
def fetch_data_from_cache(key):
raise KeyError(f"Key '{key}' not found in cache")
def get_user_profile(user_id):
try:
fetch_data_from_cache(user_id)
except KeyError as e:
raise RuntimeError("Failed to resolve user profile") from e
try:
get_user_profile("user_12345")
except RuntimeError as exc:
# The bottom of the chain is what you caught — not the root cause
print(f"Caught: {exc}")
# Walk up the chain to find the original failure
root = exc
while root.__cause__ is not None:
root = root.__cause__
print(f"Root cause: {type(root).__name__}: {root}")
# Root cause: KeyError: "Key 'user_12345' not found in cache"
# Fix the root, not the symptom
Accessing __cause__ programmatically (or reading from the top of the printed traceback) exposes the original exception before it was caught and re-raised. Fixing the root cause — the KeyError from the cache miss — eliminates the chain entirely, rather than patching the RuntimeError wrapper that is merely a symptom.
How This Fails in Real Systems
An API crashed inside a third-party payment callback. The developer looked only at the bottom line of the logs, which said 'KeyError: status', and spent 8 hours rewriting local parsing logic. In reality, the top of the chain contained an SSLError indicating the outbound connection failed, which had been caught and re-raised as a generic KeyError.
Key Takeaway
The above exception was the direct cause of the following exception: section, which points to the original, underlying problem.