---
title: "When a bug wears the fallback's message"
description: "A parcel tracker reported missing credentials for every parcel. The credentials were there. A broad except clause kept turning each attempted fix into the same reassuring sentence."
date: 2026-08-27
lang: en
canonical: https://goingstellar.ro/research/when-a-bug-wears-the-fallback-message/
author: Going Stellar
publisher: GOING STELLAR S.R.L.
topic: Failure analysis
license: CC BY 4.0
alternate_ro: https://goingstellar.ro/ro/cercetare/cand-eroarea-imbraca-mesajul-caii-de-rezerva/
---

# When a bug wears the fallback's message

*A parcel tracker reported missing credentials for every parcel. The credentials were there. A broad except clause kept turning each attempted fix into the same reassuring sentence.*

> Failure analysis · 2026-08-27 · https://goingstellar.ro/research/when-a-bug-wears-the-fallback-message/

## Key points

- A broad except clause in a fallback path converts every bug inside the try block into the fallback's own calm message, such as data unavailable or credentials missing.
- A crash points at a line of code, while an empty result points at a database, a config file or somebody else's system, so confusing the two turns a short investigation into days of work.
- An except clause in a fallback path should name the one failure the fallback was written to survive, and nothing else.
- When two stacked causes produce the same symptom, fixing only one changes nothing visible, so an unchanged output is not proof that the diagnosis was wrong.
- Verify a fix by its effect, such as a real parcel with a status attached, rather than by the absence of an error message.

## A crash and an empty result are different facts

A multi-courier parcel tracker running on a server reported the same thing for every parcel it was given: credentials missing. That is a specific claim. It says the code ran, reached the point where it needed a secret, looked, and found nothing. Read that way the fix is an operations task — put the credentials where the tracker expects them — and it is not obviously urgent, because nothing is broken, only unconfigured.

None of that was true. The credentials were present. The code never reached the point the message implied. The tracker crashed inside the path that loads a helper, and the crash was caught and converted into an empty result, which the caller then reported as an absence of data.

The gap between those two readings — *it crashed* versus *it found nothing* — is the whole article. A crash points at a line. An empty result points at a database, a config file, an ops process, somebody else's system. One of those is a short investigation. The other is days of work.

## Two causes, stacked

The tracker computed the path to a helper by walking up a fixed number of directories from its own file. In the source tree where it was written, that was correct. On the server the files were deployed flat, so the same computation walked past the top and produced a path at the filesystem root — a path that could not exist.

The fix was not clever: search a list of candidate locations and take the first one that is there. A neighbouring script in the same directory already did exactly that. The old code was not wrong when it was written; it encoded an assumption about layout, and deployment quietly broke the assumption without touching the code.

The second cause sat underneath the first. The tracker asked the credential store for a single entry holding the courier credentials. That entry had never existed. The store keeps them separately, one entry per courier. Nothing was missing; the tracker was asking for a name nobody had ever written.

Both causes produce the same symptom, so repairing either one alone changes nothing you can see from outside. That is worth stating plainly, because *I fixed it and the output did not change* is normally read as evidence that the diagnosis was wrong.

## Where we were wrong: three attempts, one visible outcome

There were three attempts. The first fix called a function from a module that was not imported in that file. At runtime that raises a name error — in Python, a **NameError**, thrown at the moment the line executes rather than when the file is loaded.

The name error was raised inside the try block of the fallback, and the fallback's handler was **except Exception: continue**. A name error is an exception. So the fix ran, failed silently, the loop moved to the next candidate, and the tracker reported exactly the same credentials-missing message as before.

Two consecutive attempts looked identical from the outside. The natural conclusion at that point is that the diagnosis is wrong and the real cause is somewhere else. That conclusion was itself wrong. The diagnosis had been right both times; the evidence we were reading was produced by the handler, not by the system. We were reading the fallback's opinion of the world and treating it as an observation of the world.

The change that mattered was to narrow the handler to the single failure the fallback exists to survive: a file that is not present. Everything else now propagates. On the next run the name error appeared, and the name error named the line.

## An except clause should name the failure it exists for

The rule that comes out of this is narrow enough to apply mechanically. An except clause in a fallback path should name the one failure the fallback was written to handle, and nothing else.

A broad handler in that position does not make a system robust. It converts every bug inside the try block into the fallback's own message. The bug can be a typo, a missing import, a renamed attribute, a wrong argument count — all of them come out the far side wearing the same sentence.

The asymmetry is what makes it expensive. The person writing the fallback has one benign case in mind, and the message they write is calm: data unavailable, source empty, nothing found. Everyone who later reads that output inherits the expectation, including the author. Catching broadly is a claim that you anticipated every failure inside the block, and it is almost never a claim anyone intended to make.

## How to run the same check

Search your codebase for broad exception handlers whose body is a continue, a pass, or a return of an empty value — an empty list, an empty dict, a null, an empty string. The body is the tell, not the clause: a broad handler that logs the exception and re-raises is a different thing and is usually fine.

For each hit, ask what specific failure it was written for, and narrow the clause to that failure. If you cannot name the failure, the handler is not handling anything. It is hiding something, and what it hides is whichever bug lands inside the block next.

Then verify by effect, not by absence. Run the thing and require a real result — a parcel with a status attached — rather than the absence of an error message. Throughout this incident the process exited cleanly and printed a sentence that parsed as an explanation; a check that asserted only *it did not crash* was true the entire time and told us nothing.

## Limitations

Narrowing a handler surfaces errors that were previously invisible. On the day you deploy it, that looks like a regression: new tracebacks, often in code you did not touch. It is not a regression, it is the handler working, and the deployment should be planned expecting it rather than rolled back on the first traceback.

The examples here come from one script. We have not measured how common the pattern is across the rest of the code, and this article makes no claim about a rate. The pattern is not language-specific, though the exact syntax is.

Narrowing also requires judgement, and judgement can be wrong in the other direction. Name the exception too tightly and a legitimately benign failure starts propagating into production. The test for that is the same question as before: can you say out loud which failure the fallback exists to survive? When the answer changes, the clause changes with it.

## Questions and answers

### Why is catching every exception in a fallback a bad idea?

Catching every exception in a fallback is a bad idea because the broad handler converts any bug inside the try block into the fallback's own message. A typo, a missing import, a renamed attribute or a wrong argument count all come out wearing the same calm sentence, such as data unavailable. Catching broadly is an unintended claim that every failure inside the block was anticipated.

### How do I find risky exception handlers in a codebase?

Risky exception handlers are broad handlers whose body is a continue, a pass, or a return of an empty value such as an empty list, empty dict, null or empty string. The body is the tell, not the clause. A broad handler that logs the exception and re-raises it is usually fine. For each risky handler, name the specific failure it was written for and narrow the clause to that failure.

### Why did my fix not change the error message?

A fix may not change the error message when a broad except clause swallows the new error and prints the old fallback message again. A NameError raised inside the try block is still an exception, so the loop silently moves on. Two stacked causes with the same symptom also hide a partial fix. Narrowing the handler lets the real error surface and name the line.

### Is narrowing an exception handler a regression when new tracebacks appear?

Narrowing an exception handler is not a regression when new tracebacks appear; it is the handler working as intended. Errors that were previously invisible start to surface, often in code nobody touched. The deployment should be planned expecting this rather than rolled back on the first traceback. Naming the exception too tightly can, however, let a legitimately benign failure propagate into production.

### How should I verify that a bug fix actually worked?

A bug fix should be verified by its effect, not by the absence of an error. Run the code and require a real result, for example a parcel with a status attached. A check that only asserts the process did not crash can stay true the whole time while a broad handler hides the failure behind a sentence that reads like an explanation.

## How to cite

**Cite this as:** Going Stellar (2026). *When a bug wears the fallback's message*. GOING STELLAR S.R.L.. https://goingstellar.ro/research/when-a-bug-wears-the-fallback-message/

```bibtex
@misc{goingstellar2026when,
  title  = {When a bug wears the fallback's message},
  author = {{Going Stellar}},
  year   = {2026},
  url    = {https://goingstellar.ro/research/when-a-bug-wears-the-fallback-message/},
  note   = {2026-08-27}
}
```

Licence. Free to quote with attribution.

## Also available

- This page as HTML: https://goingstellar.ro/research/when-a-bug-wears-the-fallback-message/
- Same article in Romanian: https://goingstellar.ro/ro/cercetare/cand-eroarea-imbraca-mesajul-caii-de-rezerva/
