A Silent Fallback Becomes a Silent Lie
A try/catch swallowed a value that did not exist. The placeholder it produced ended up in the photo we used to sell the exact feature it had failed to render.
HappyBirthTown is a pixel town where friends gather for someone's birthday. It has a generator that bakes invitation cards one at a time, and inside that generator sits a table of cake flavours. One row of the table said mint.
There is no mint cake in the product. There are five: cream, choco, strawberry, cheese, matcha. The function that draws the art received that value and threw. The try/catch wrapped around it swallowed the throw, and an emoji dropped into the space where the cake should have been. The fallback was deliberate — if the art cannot be baked, use an emoji. Nobody thought it looked wrong, and it stayed that way for close to five months.
The swallowed exception did not stay inside the code
This was not found through a bug report. It surfaced while reviewing marketing material.
Output from the invitation generator had been used in a social post, and the thing that post was selling was the flavour you choose stands on your card. In the photo attached as evidence for that sentence, the cake was somebody else's emoji. The product had manufactured a picture boasting about a capability it had just failed to exercise.
The part worth pausing on is not that an exception was swallowed. It is that the swallowed exception produced an artifact, and the artifact left the repository. Nobody lied. The system did.
"Not yet" and "no such thing" are different failures
The fallback itself was a correct design. Art genuinely does arrive late sometimes, and an emoji standing in beats an empty rectangle.
What was wrong is that the fallback gave two completely different failures the same face.
- Not yet — waiting fixes it. This is exactly what a fallback is for.
- No such value — waiting never fixes it. A person has to.
Both went into the same catch and came out as the same emoji cake. That is how the second kind of failure got to impersonate the first for five months. We knew a fallback existed, so an emoji cake read as the art must not be ready yet.
An honest door in front of the fallback
We did not remove the fallback. We put two doors in front of it.
At the place where the value is written — the flavour table — an assertion now throws when an entry is missing. That single assertion forced every one of the seven towns to be spelled out explicitly, and in the course of spelling them out, an entry like mint could no longer survive.
At the place where the value is received — the scene builder — there is now a function that washes the input. If the value is not a flavour the product knows, the whole sprite atlas fails to bake and the screen comes up empty. We chose the loud failure on purpose: an empty screen always gets reported, and an emoji cake never does.
Watch the places where your output becomes someone else's input
I think this holds well outside this product. A swallowed exception becomes a fact over time. It leaves no log line, fires no alert, and the screen stays plausibly full, so nobody calls it an incident. Eventually the stand-in hardens into the expected behaviour.
The genuinely dangerous version is wherever your output becomes someone else's raw material — asset generators, reports, exports. What comes out of those gets no code review and has to pass no test. It just leaves as a file and becomes evidence for the sentence this is what our product looks like.
If a fallback belongs somewhere, split the reason it fired. Not yet, or wrong? Failing loudly on the second one is far cheaper than recalling a false piece of evidence five months later.