Re-raising and exception chaining
Re-raising: handle something, but let the failure continue
Sometimes you want to react to an exception — log it, clean something
up — without actually treating it as handled. A bare raise, with no
argument, inside an except block re-raises the exact exception that was
just caught, continuing on exactly as if that except block wasn’t there
at all:
The log line prints, and the original ValueError still propagates
afterward, uncaught, exactly as it would have without the try block at
all — run_with_logging observed the failure without hiding it from
whatever called it.
Wrapping in a new exception: the implicit chain
Instead of re-raising the same exception, you can raise a different one — commonly, a custom exception from the previous section, to translate a low-level failure into something more meaningful for whoever’s calling your code:
Notice Python shows both tracebacks — the original ValueError and the
new ConfigError — connected by “During handling of the above exception,
another exception occurred.” This happens automatically any time you
raise a new exception from inside an except block; Python remembers
what was being handled and shows it for context, even though you never
asked it to.
Making the connection explicit: raise ... from
The automatic chaining above is useful, but it can also happen by
accident — any exception raised inside an except block gets chained
this way, whether or not the two are actually related. raise NewError(...) from original makes the relationship deliberate and explicit instead,
and produces a clearer message specifically framing it as a cause:
“The above exception was the direct cause of the following exception” —
compare that to the plain “During handling of…” from before: from e
tells both Python and, more importantly, the next person reading this
traceback that the ConfigError isn’t incidental, it’s a deliberate
translation of that specific ValueError. The debugging value is real:
the original low-level cause (a bad string) is preserved right alongside
the higher-level, more meaningful error your code actually raised.
Catching more than one exception type at once
Sometimes several different exception types should be handled the same
way — you’ve already seen every type get its own separate except
clause; a tuple lets one clause catch several types identically:
except (json.JSONDecodeError, TypeError): catches either type with one
block — json.loads(None) raises TypeError rather than
JSONDecodeError, but here both are treated identically, so listing them
in one tuple avoids writing the same return {} twice under two separate
except clauses.
What does a bare raise, with no arguments, do inside an except block?
Open and parse the file at path. Catch FileNotFoundError and json.JSONDecodeError separately (not in one tuple, since each needs its own message), and in each case raise ConfigError with the specified message, chained with 'from' to the original exception.