Keep finally cleanup from masking the main error
To keep a finally cleanup from masking the main error, catch and record the cleanup failure inside finally, then let the original exception continue outward. A finally suite always runs; if it raises a new exception, that new exception becomes the exception observed by the caller. The first half of this synthetic fixture demonstrates that failure: unsafe_operation() starts with ValueError("main failed"), but cleanup() raises RuntimeError("cleanup failed"), so the caller receives RuntimeError.
protected_operation() changes only the cleanup boundary. Its nested try calls cleanup, and its narrowly scoped except RuntimeError appends a diagnostic event. It does not return, and it does not raise a replacement exception. Therefore, after finally completes, the original ValueError remains active. The assertions check both independently: the event log records the cleanup problem, while the caught primary exception is a ValueError with its original message.
This pattern is useful when the work failure is the diagnostic your caller must handle, while cleanup failure is secondary operational evidence. It does not make cleanup succeed or recover resources; it only preserves the chosen exception ownership. Consider reporting the recorded cleanup event through the logging mechanism appropriate to the application. Python’s try documentation describes finally execution and exception propagation; contextlib documents context-manager utilities for cases where a resource can instead own its cleanup protocol. Python error handling tutorial and contextlib documentation.
AI-assistance disclosure: This article was drafted with AI assistance and uses a synthetic, no-I/O example.
events = []
def cleanup():
raise RuntimeError("cleanup failed")
def unsafe_operation():
try:
raise ValueError("main failed")
finally:
cleanup()
def protected_operation():
try:
raise ValueError("main failed")
finally:
try:
cleanup()
except RuntimeError as cleanup_error:
events.append(f"cleanup error: {cleanup_error}")
try:
unsafe_operation()
except RuntimeError as error:
print(f"unsafe raised: {type(error).__name__}: {error}")
try:
protected_operation()
except ValueError as error:
primary_error = error
assert events == ["cleanup error: cleanup failed"]
assert type(primary_error) is ValueError
assert str(primary_error) == "main failed"
print(f"cleanup events: {events}")
print(f"safe raised: {type(primary_error).__name__}: {primary_error}")
unsafe raised: RuntimeError: cleanup failed
cleanup events: ['cleanup error: cleanup failed']
safe raised: ValueError: main failed