with-Block close() Exception Masks the Original Error (tested)
A with open(path, "w") as f: block's __exit__ always calls f.close() when the block ends, whether it ends normally or because an exception was raised inside it. close() on a buffered writer flushes whatever is still sitting in the buffer, and that flush can fail for completely ordinary reasons -- a full disk, a broken pipe, a remote filesystem going away. When that happens while an exception is already propagating out of the with block, Python lets the close() failure replace the original exception as what the caller actually catches. The original error isn't gone -- it's chained onto the new one's __context__ -- but any code that only logs str(e) or checks the exception type will see the close()-time error and never learn what actually went wrong first.
1. A write succeeds, but close() fails during normal exit
import io
class FailOnClose(io.RawIOBase):
def writable(self):
return True
def write(self, b):
return len(b)
def close(self):
super().close()
raise OSError("disk full on close")
raw = FailOnClose()
try:
with io.TextIOWrapper(io.BufferedWriter(raw), encoding="utf-8") as f:
f.write("hello")
except OSError as e:
print("no exception inside the block, just this ->", e)
no exception inside the block, just this -> disk full on close
Measured: with nothing else going wrong, the close() failure surfaces on its own, which is exactly what you'd expect.
2. Raising your own exception inside the block gets replaced by the close() error
raw2 = FailOnClose()
try:
with io.TextIOWrapper(io.BufferedWriter(raw2), encoding="utf-8") as f:
f.write("hello")
raise ValueError("the real bug: something went wrong processing the data")
except Exception as e:
print("what the caller actually sees:", type(e).__name__, "->", e)
what the caller actually sees: OSError -> disk full on close
Measured: the ValueError that was actually raised inside the block -- the real bug -- never reaches the except clause at all. __exit__ calls close() while the ValueError is unwinding, close() raises its own OSError, and that OSError is what propagates instead.
3. The original exception still exists -- buried in __context__
raw3 = FailOnClose()
try:
with io.TextIOWrapper(io.BufferedWriter(raw3), encoding="utf-8") as f:
f.write("hello")
raise ValueError("the real bug: something went wrong processing the data")
except Exception as e:
print("caught:", type(e).__name__, e)
print("original error, only here:", type(e.__context__).__name__, "->", e.__context__)
caught: OSError disk full on close
original error, only here: ValueError -> the real bug: something went wrong processing the data
Measured: the original ValueError is chained onto __context__ -- Python's default traceback printer shows both with "During handling of the above exception, another exception occurred", but any code that catches the exception and only inspects it directly (logging frameworks, except ValueError: type checks, retry logic keyed on exception type) never sees it.
4. The fix: check __context__/__cause__ when handling exceptions from a with block that does I/O
raw4 = FailOnClose()
try:
with io.TextIOWrapper(io.BufferedWriter(raw4), encoding="utf-8") as f:
f.write("hello")
raise ValueError("the real bug: something went wrong processing the data")
except Exception as e:
root = e
while root.__context__ is not None:
root = root.__context__
print("actual root cause:", type(root).__name__, "->", root)
actual root cause: ValueError -> the real bug: something went wrong processing the data
Measured: walking __context__ back to the end recovers the real error. If a flush/close failure during cleanup is expected to be secondary to whatever bug you're actually trying to diagnose, log or inspect __context__ explicitly instead of trusting that the caught exception is the one that was originally raised.
Frequently asked questions
Does this only happen with custom RawIOBase subclasses?
We used a custom subclass to make close() reliably fail on demand, but the mechanism is generic Python exception-chaining behavior in any with block (__exit__ raising while an exception is already propagating) -- it isn't specific to io or to our test class. A real file's close() failing on a full disk or a network filesystem going away triggers the identical replacement.
Is the original exception actually lost?
No -- we measured it's preserved as __context__ on the new exception, and Python's default unhandled-exception traceback prints both. It's only 'lost' in the sense that an except clause that catches and inspects the exception directly, without walking __context__, never sees it.
Does using except ValueError specifically still work if the real error was a ValueError?
No -- we measured that the caught exception's type is whatever close() raised (OSError in our repro), not the type of the exception that was originally raised inside the block, so a type-specific except clause for the original error type won't match at all.
Is there a way to make the with block prefer the original exception?
Not from the caller's side without walking __context__ -- the replacement happens inside __exit__'s own control flow before your except clause ever runs. The only generically reliable fix is to always check __context__ when handling an exception from code that does I/O inside a with block.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in io.TextIOWrapper/io.BufferedWriter wrapping a custom RawIOBase whose close() raises after a successful write(), inside a with block that also raises its own exception. PlainHeaders is our extension; Python's io module is built in.