What is exception chaining (`raise ... from ...`), and why does it matter?

6 minintermediateexceptionsexception-chainingraise-from

Quick Answer

When you raise a new exception while handling another (inside an `except` block), Python automatically records the original as `__context__` (implicit chaining) — printed as "During handling of the above exception, another exception occurred." Using `raise NewError(...) from original_error` sets `__cause__` explicitly, marking the relationship as intentional ("the direct cause was...") rather than incidental, and both are preserved in the traceback for debugging.

Detailed Answer

Implicit chaining: happens automatically

def parse_config(raw):
    try:
        return int(raw)
    except ValueError:
        raise RuntimeError("config value must be numeric")

parse_config("abc")
ValueError: invalid literal for int() with base 10: 'abc'

During handling of the above exception, another exception occurred:

RuntimeError: config value must be numeric

Because RuntimeError was raised inside the except ValueError: block, Python automatically links the original ValueError as the new exception's __context__ — the full traceback shows both, which is invaluable for debugging (you see not just "config is bad" but why, down to the actual parse failure).

Explicit chaining with from: marking intent

def parse_config(raw):
    try:
        return int(raw)
    except ValueError as e:
        raise RuntimeError("config value must be numeric") from e
ValueError: invalid literal for int() with base 10: 'abc'

The above exception was the direct cause of the following exception:

RuntimeError: config value must be numeric

from e sets __cause__ explicitly, and Python's traceback formatting changes to "was the direct cause of" — signaling this chaining was deliberate (a real causal relationship), not just "this happened to occur while handling something else." Tools and some linters distinguish __cause__ (explicit) from __context__ (implicit) accordingly.

Suppressing chaining entirely: from None

def parse_config(raw):
    try:
        return int(raw)
    except ValueError:
        raise RuntimeError("config value must be numeric") from None
RuntimeError: config value must be numeric

from None suppresses the chained traceback entirely — useful when the original low-level exception is genuinely irrelevant noise for the caller (e.g., a public API deliberately hiding internal implementation exceptions behind a clean, documented error type).

Why this matters beyond cosmetics

Exception chaining preserves the full causal chain for debugging — without it, catching a low-level error and re-raising a higher-level one (a very common pattern for wrapping errors in domain-specific exception types) would silently discard the original error's message and traceback, making production debugging much harder ("config value must be numeric" tells you what failed, but not why the int conversion failed).

Interview-ready summary: Raising an exception inside an except block automatically chains it via __context__; raise ... from cause sets __cause__ explicitly to mark the relationship as intentional ("direct cause"), and raise ... from None suppresses chaining when the original exception is genuinely irrelevant to expose. Either way, chaining keeps the full failure history visible instead of discarding it when wrapping low-level errors in higher-level ones.