Loading Python runtime…
Let's say a circus puts up a safety net under a trapeze act.
The net is sized for one specific case: if the flyer misses their catch, they land in it safely.
That's try / except. Wrap the risky part in a try block, and if it fails the way you expected, the matching except clause catches the fall instead of the crash reaching the audience.
Now someone decides the net isn't safe enough, so they stretch it to cover the entire stage. A stagehand ten feet away trips over a cable and falls too, right into the same net.
try:
charge_card(customer)
except Exception:
return {"status": "declined"}
# catches the declined card, and the unrelated bug, identically
From the audience, nothing looks different. Somebody fell. Somebody landed in the net. The show goes on, and nobody notices there's now a tripping hazard on stage, because the net quietly caught the evidence along with the performer.
So far, that's try/except working exactly as intended: a specific failure, handled gracefully. Silent error swallowing starts one decision later, not whether to catch a failure, but how wide a net you cast when you do.
An except clause is a filter, not a floor. It only stops what you actually name. except PaymentDeclinedError: matches that one thing (and its subclasses) and lets every other kind of failure keep propagating upward, crash and all. except Exception:, or worse, a bare except:, matches almost everything, because in Python nearly every runtime failure is some kind of Exception. The line between "defensive" and "a trapdoor for every bug in this block" is exactly how wide you make that net.
Broad catches feel responsible. Nobody wants a single edge case to crash a checkout, a login, a whole request. So a function that's supposed to handle "the card was declined" quietly grows into a function that handles "anything that goes wrong here, ever." That expansion usually isn't a decision anyone makes on purpose. It's except SomeSpecificError widened to except Exception under deadline pressure, "just to be safe," and left that way.
A broad catch can't tell your bug from the failure it was written for. Here's the part that actually bites. Once that block reads except Exception: return None, a totally unrelated defect (a typo'd dictionary key, a value that's a string instead of a number, an assumption that turned out to be wrong) raises an exception too. It gets caught by the exact same block, and comes back out looking exactly as "handled" as the case you actually designed for. Nothing at the call site can tell the difference.
The trap: code that fails by quietly succeeding. The function doesn't crash. It doesn't log anything alarming. It just returns a plausible-looking default (None, 0, an empty list, a status that reads "declined") and every caller downstream treats that default as real data, because nothing about it looks like a failure. The bug isn't that the code broke. It's that the code kept running and never told anyone it was wrong.
Don't take our word for it. Flip the except clause yourself and watch what comes back.
Flip between a narrow and a broad except clause, then trigger a failure. Watch what the function actually returns.
Switch to the broad except clause, trigger one of the two bug scenarios, and see what comes back before continuing.
Loading Python runtime…