What happens if __exit__ itself raises while the with body is already unwinding an exception?
answer
- Two exceptions, only one propagates
- The later one wins
- The earlier one is not deleted, only demoted
- Outer except clauses stop matching the original
- Branch on exc_type is None inside __exit__
basics
~20 sThe cleanup exception wins: it propagates and replaces the original, which survives only as the new exception's context and prints under "During handling of the above exception, another exception occurred". Callers matching the original type stop matching.
solid answer
~50 sAn exception raised inside `__exit__` supersedes the one from the body. The original is not lost - Python sets it as the new exception's `__context__`, so the traceback shows both under "During handling of the above exception, another exception occurred" - but it is no longer the exception being propagated, so an outer `except ValueError` that would have caught the body's error now misses it entirely. That is how a flaky teardown can rewrite every alert in a batch job. The fix is to make cleanup defensive: wrap the risky part of `__exit__` in its own `try`/`except`, and when `exc_type is not None` log the cleanup failure and return falsy so the body's exception continues; only re-raise from `__exit__` when the block was otherwise succeeding. If you deliberately want both, `raise CleanupError(...) from exc` sets `__cause__` and makes the relationship explicit.
code
python · 14 linesclass FlushOnExit:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
raise RuntimeError("flush failed")
try:
with FlushOnExit():
raise ValueError("parse failed")
except RuntimeError as err:
print("propagated:", err)
print("original kept as context:", repr(err.__context__))go deeper
Know the headline: if the cleanup code in __exit__ raises, that is the error you see, and the original one from the block is only shown as extra context above it in the traceback.
Explain the chaining mechanics - the new exception propagates, the old one is attached as __context__, and raise ... from sets __cause__ instead. Be able to read a two-part traceback out loud and say which exception the caller actually caught.
Show that you have operated this: a raising teardown rewrites which handlers match and which alerts fire, so cleanup should branch on exc_type is None, log its own failure while unwinding, and return falsy so the diagnostic exception survives.
Own the failure-visibility contract. Decide when a cleanup failure is allowed to outrank a body failure (a failed rollback usually is), require deliberate raise ... from chaining rather than accidental context, and make sure error routing is not silently keyed on types that teardown can replace.
`__exit__` runs while an exception is in flight, which makes it one of the few places in Python where two exceptions genuinely compete. The rule is simple and the operational consequences are not. ## The rule If `__exit__` raises, that new exception propagates out of the `with` statement and the body's exception is replaced. The return value never matters, because the method never returned. The original is preserved as implicit context: ```python try: with FlushOnExit(): # __exit__ raises RuntimeError raise ValueError("parse failed") except RuntimeError as err: err.__context__ # ValueError('parse failed') ``` Every exception object carries `__context__` (set automatically when one exception is raised during the handling of another), `__cause__` (set only by `raise ... from`), and `__suppress_context__` (set to True by `raise ... from None`). The default traceback printer walks the chain and prints the older exception first, followed by "During handling of the above exception, another exception occurred". ## Why this is a production problem, not a trivia question Consider a nightly geocoding batch: a six-hour run over address records, with a context manager that opens an output writer and flushes it in `__exit__`. One record carries a locale-dependent coordinate format - decimal commas where the parser expects points - and the body raises `ValueError`. The manager's flush then fails too, because the partially written buffer is inconsistent, and raises `OSError`. What the on-call engineer sees is an `OSError` about a write. The `ValueError` that actually caused the run to fail is three lines up in the traceback, under a sentence people skim past, and the error-routing rule that pages the data team on `ValueError` never fires. Worse, if the outer code has `except ValueError: skip_record()`, the batch no longer skips - it dies. A cleanup bug has silently changed the failure semantics of the whole job. ## Writing cleanup that does not steal the exception The discipline: **`__exit__` should not raise while unwinding unless it has something more important to say.** Concretely, guard the risky part and branch on whether an exception is already in flight: ```python def __exit__(self, exc_type, exc, tb): try: self.writer.flush() except OSError as cleanup_error: if exc_type is None: raise # nothing else was wrong; this IS the failure logging.warning("flush failed while unwinding: %s", cleanup_error) return False # body's exception keeps propagating ``` On a clean exit the flush failure is the news, so it propagates. On a failing exit the original diagnosis is more valuable, so the cleanup error goes to the log and the body's exception survives. If you want both visible to the caller and are willing to change the propagated type, be explicit: `raise CleanupError("flush failed") from exc` sets `__cause__`, which prints as "The above exception was the direct cause of the following exception" and reads as intent rather than accident. ## Two adjacent traps **Returning from a `finally` inside `__exit__`.** A `return` that leaves a `finally` block discards any exception in flight through that block - including one raised by the cleanup code you were trying to protect. Python 3.14 added a `SyntaxWarning: 'return' in a 'finally' block` (PEP 765) precisely because this silently eats errors. Put the `return` after the `try` statement instead. **Suppressing by accident during the cleanup handler.** Writing `except Exception: pass` inside `__exit__` hides teardown failures completely; if the writer never flushed, the job reports success and produces a truncated file. Log it, count it, or re-raise - never swallow silently. ## What an interviewer is listening for Three beats. First the mechanic: the cleanup exception replaces the body's exception and the original becomes `__context__`. Second the consequence: outer handlers and alert routing keyed on the original type stop matching, so the observable failure mode of the system changes. Third the remedy: branch on `exc_type is None` inside a defensive `try`/`except` in `__exit__`, and use `raise ... from` when chaining is deliberate. A candidate who only recites the first beat has read the data model; a candidate who volunteers the second has debugged a batch job at 3am.
- How does __context__ differ from __cause__ on the propagated exception?`__context__` is set automatically whenever an exception is raised while another is being handled - nobody asked for it, it just records what was in flight. `__cause__` is set only by an explicit `raise New(...) from old`, and it also sets `__suppress_context__`, so the traceback prints "direct cause" rather than "during handling". Use `from` when the chaining is a deliberate statement about causality.
- When is it correct for __exit__ to raise while an exception is already propagating?When the cleanup failure is genuinely more serious than the body's error - a transaction rollback that itself failed, for example, means the data may now be inconsistent, which outranks the query error that triggered it. Raise deliberately with `from exc` so the original is chained as `__cause__`, and make sure the new exception type is one your callers actually route on.
- Why did Python 3.14 start warning about a return inside a finally block?Because leaving a `finally` block via `return`, `break` or `continue` discards any exception passing through it - the control transfer wins and the error vanishes with no trace. PEP 765 made that a `SyntaxWarning` in 3.14. Inside `__exit__` it is especially damaging: the method looks like ordinary cleanup while silently eating both the body's exception and its own.
It is like a paramedic tripping over the stretcher: the ambulance now reports a paramedic injury, and the patient who was actually dying is a footnote in the report.
saying these in an interview costs you the question
- Says the original exception is lost entirely
- Thinks both exceptions propagate to the caller
- Believes the body's exception wins over the cleanup one
- Wraps cleanup in except Exception: pass to be safe
- Cannot name __context__ or explain the chained traceback
- Assumes an outer except on the original type still matches