Debugging & Performance

Decoding "During Handling of the Above Exception"

Python prints two different sentences between chained exceptions, and they mean different things. The above exception was the direct cause of the following exception: says someone deliberately converted one error into another with raise NewError(...) from err. During handling of the above exception, another exception occurred: says an exception escaped from an except or finally block while the first one was still being handled — which is often not deliberate at all, and frequently means the error handler has its own bug.

Reading the two correctly changes where you look. A direct-cause chain usually has its root cause at the top, with the wrapper at the bottom describing it at a higher level. A during-handling chain often has two problems: the original failure at the top, and a second failure in the code that was supposed to deal with it at the bottom. Fixing only the bottom one leaves the original error unhandled; fixing only the top one leaves a fragile handler that will break again next time.

Prerequisites

Solution

Python
# The three chaining forms side by side.
import json

def load_config_implicit(path):
    try:
        return json.load(open(path))
    except json.JSONDecodeError:
        log.error("bad config at %s", pth)          # typo → NameError inside handler

def load_config_explicit(path):
    try:
        return json.load(open(path))
    except json.JSONDecodeError as err:
        raise ConfigError(f"invalid JSON in {path}") from err

def lookup_plan(plans, name):
    try:
        return plans[name]
    except KeyError:
        raise UnknownPlan(name) from None            # KeyError is noise for the caller
Plain text
Traceback (most recent call last):
  File "config.py", line 5, in load_config_implicit
    return json.load(open(path))
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "config.py", line 7, in load_config_implicit
    log.error("bad config at %s", pth)
                                  ^^^
NameError: name 'pth' is not defined. Did you mean: 'path'?
Implicit and explicit exception chaining Three columns show chaining forms. Implicit chaining happens when an exception escapes an except block; Python sets __context__ and prints the during-handling message. Explicit chaining with raise from err sets __cause__ and prints the direct-cause message. raise from None sets __suppress_context__ so only the new exception is printed. Three ways one exception follows another implicit error escapes except/finally sets __context__ "During handling of the above exception…" often a handler bug raise … from err deliberate wrapping sets __cause__ "The above exception was the direct cause…" intended translation raise … from None hide the internal error __suppress_context__ = True only the new exception is printed context still on the object
The separator sentence tells you which column you are in — and therefore whether the bottom exception was intended.

Why this works

Every exception object carries three attributes that describe its relationship to others. __context__ is set automatically whenever an exception is raised while another is being handled — inside an except block, or inside a finally block during unwinding. __cause__ is set only by raise X from Y. __suppress_context__ is set to true by any raise ... from ..., including from None, and tells the traceback printer to skip the implicit context.

The traceback printer walks the chain from the propagated exception backwards and prints oldest first. If __cause__ is set, it prints the cause with the direct-cause separator. Otherwise, if __context__ is set and not suppressed, it prints the context with the during-handling separator. That is why the order on screen runs from the first failure at the top to the propagated one at the bottom: the bottom exception is the one your except clauses — or pytest — actually saw.

In the example, the program failed with NameError, not JSONDecodeError. A caller with except JSONDecodeError would not catch it. That is the practical danger of implicit chains: a bug in a handler changes the type of the error that escapes, and code written to handle the original type silently stops working.

Reading chains inside pytest

pytest prints chains the same way, with its own formatting. With the default --tb=auto, each exception in the chain gets its own section, separated by the same sentences, and the final section carries the E lines of the propagated exception. Two things are worth knowing.

First, pytest.raises matches the propagated exception only. with pytest.raises(JSONDecodeError) fails on the implicit chain above, because what escaped was a NameError. If a test that used to pass starts failing that way, read the chain: the handler probably broke.

Second, --tb=short and --tb=line shorten or drop the chain entirely. When a failure is confusing, re-run with --tb=long to see every frame of every exception, or use --full-trace to include pytest's own frames as well.

Reading order for a chained traceback A chained traceback is shown as two stacked blocks. The bottom block is the propagated exception and is read first because it is what callers and pytest saw. The top block is the original exception and is read second to find the root cause. The separator between them tells whether the chain was deliberate. Read bottom, then separator, then top original exception root cause lives here separator: deliberate or accidental? propagated exception what callers and pytest saw 1 2 3
Start from what escaped, decide whether the chain was intended, then go up to the original failure.

Writing handlers that chain on purpose

Most implicit chains are accidents, and the fix is to make every handler's intent explicit. Three rules cover nearly every case.

Translate with from err. When a lower layer's exception should become a higher layer's error — a JSONDecodeError becoming a ConfigError, a database IntegrityError becoming a DuplicateUser — always write raise NewError(...) from err. The traceback then says "direct cause", which tells the next reader the translation was deliberate, and err.__cause__ gives programmatic access to the original for logging or retry decisions.

Hide with from None, sparingly. When the lower-level exception is an implementation detail that adds nothing — an internal KeyError from a dict lookup that the caller only knows as "unknown plan" — from None keeps the caller's traceback short. Do this only where the original genuinely has no diagnostic value; hiding an OSError with an errno is a false economy.

Keep handlers boring. The during-handling message usually appears because a handler did something that could fail: formatted a message with a misspelled variable, called a cleanup function that raised, logged an object whose __repr__ raised. Handlers should do as little as possible, and anything risky inside them — closing a connection, sending a metric — should be wrapped in its own try so a failure there cannot replace the original error.

Python 3.11's add_note offers a fourth option that avoids chaining altogether. Instead of wrapping an exception to add context, add a note and re-raise the original: err.add_note(f"while loading {path}") then raise. The traceback shows the original exception with the note underneath, the type is unchanged, and callers' except clauses keep working.

Choosing how a handler should raise A decision flow for handlers. If the caller should see a different error type, raise the new error from the original. If the original is pure noise, raise from None. If you only want to add context, add a note and re-raise the original unchanged. Risky work inside the handler gets its own try block. Make every handler's intent explicit new error type raise New() from err direct cause shown original is noise raise New() from None short traceback just add context err.add_note(…); raise type unchanged risky cleanup own try/except inside can't replace the error
With these four habits, a "during handling" message in your own code becomes rare enough to be a reliable signal of a real handler bug.

Testing the chain itself

When a function's contract says it raises ConfigError for invalid files, the chain is part of that contract too: callers may log err.__cause__ or branch on it. A small test pins the behaviour so a future refactor cannot quietly turn a deliberate translation into an accidental one.

Python
def test_invalid_json_is_translated(tmp_path):
    bad = tmp_path / "c.json"
    bad.write_text("{not json")
    with pytest.raises(ConfigError) as info:
        load_config_explicit(bad)
    assert isinstance(info.value.__cause__, json.JSONDecodeError)
    assert "c.json" in str(info.value)

The first assertion checks the type callers rely on. The second checks that the cause is the original parsing error rather than something a buggy handler raised. The third checks the message is useful. A test like this would have caught the misspelled variable in the implicit example immediately: pytest.raises(ConfigError) fails when what escapes is a NameError, and the chain printed in the failure shows exactly why. Tests of this shape are cheap to write for every public function that translates errors, and they turn the chain from an accident of implementation into a documented, enforced part of the interface that reviewers can see.

Edge cases and failure modes

  • finally blocks. An exception in finally during unwinding chains implicitly, like one in except. A return in finally swallows the original exception entirely — Python 3.14 warns about it.
  • Re-raising with raise err. Re-raising the same object inside its own handler does not chain; raise alone re-raises with the original traceback, which is usually what you want.
  • Logging with exc_info=True. The logging module prints the full chain for the current exception. Logging inside a handler that then raises something else logs the first and propagates the second.
  • Exception groups. ExceptionGroup tracebacks nest sub-exceptions with box-drawing characters; each sub-exception can itself be chained. Read each branch the same way.
  • Library wrappers hiding causes. Some libraries use from None liberally. When a wrapper hides something you need, inspect err.__context__ in a debugger — it is still there.

Frequently Asked Questions

What does "During handling of the above exception, another exception occurred" mean? An exception was raised inside an except or finally block while the first exception was being handled. Python records the first one as the second's __context__ and prints both, first one on top. The second exception is the one that propagated.

What is the difference between that message and "The above exception was the direct cause"? The direct-cause message comes from raise ... from err, which sets __cause__ and says the new exception deliberately wraps the old. The during-handling message comes from implicit chaining and often signals a bug in the handler itself.

When should I use raise from None? When the original exception is an implementation detail that would only confuse the caller, such as a KeyError from an internal lookup translated into a domain error. It suppresses the context in the printed traceback while keeping it on __context__ for debugging.

← Back to Reading Tracebacks & Exception Chains