A crash is honest. It tells you something went wrong, at a line number, with a stack trace.
Silence is also a claim. It says everything went fine. That claim is far more expensive when it is false, and it is the one we are now producing at scale.
In March I ran one command and deleted 130 commits of my own history. That was not the worst thing that has happened to this project. It was loud. I knew within four seconds, spent an evening in the reflog and recovered most of it from a branch I had made by accident three weeks earlier. Loud failures announce themselves, so you fix them.
The ones that nearly finished me were quiet. A button that reported success and saved nothing. A temperature check that had been reading nothing for months. A learning engine nobody was calling. A desktop shortcut, shipped to real users through the Microsoft Store, that opened absolutely nothing.
None of them crashed. Every one of them passed every test I owned.
I have been building a Windows system monitor in public for fourteen months, alone, in the evenings after work. The day job has been a warehouse in the Netherlands, then welding plastic in a repair shop, and now a taxi. Everything is on GitHub from the first commit, so the log of my mistakes is not a memory. It is a record I cannot edit.
Six real ones, and what each taught me
Everything below happened to a real application with real users. Where I look careless, I was.
1. The button that said "Applied successfully"
There is a fan curve editor. You drag points, you click Apply, a green message appears.
The message appeared. The file was never written. Every restart threw the user's settings away in silence, and it had been doing that for two releases.
Nobody reported it, and the reason is worth sitting with: the user cannot tell the difference between "it saved" and "it said it saved." They set their curve, saw the confirmation, restarted a week later, found the defaults back, and assumed they had done something wrong.
2. The check that was measuring nothing
The proactive monitor is supposed to warn you when your machine runs hot. I wrote it, I tested it, I shipped it, and I wrote a blog post about it.
For months it was built on psutil.sensors_temperatures(). The check ran on schedule, found nothing to warn about, and reported all clear. Forever. The test suite was happy, because the tests asserted that the alert did not fire when temperatures were normal, and an alert that can never fire passes that test every time.
An empty reading is indistinguishable from a normal reading if you never assert that a reading exists.
psutil's API reference gives one line for that function: Availability: Linux, FreeBSD. Not Windows. So it does not return an empty dictionary on Windows. The attribute does not exist at all. Calling it raises
AttributeError. Here is the check, on the machine this article was written on, psutil 7.2.1 on Windows 10 build 19045:
>>> import psutil
>>> hasattr(psutil, "sensors_temperatures")
False
The bug is the same size. The lesson is bigger. It was never "an API politely handed me an empty result". It was an exception, raised on every single call, landing inside a try block that had nothing to say about it. Failure number two was standing on failure number three the whole time, and I did not see it because I kept describing it from memory. That is the entire thesis of this article, performed live at my own expense.
The code that reads temperatures today does not guess. It asks whether the function exists before calling it, and falls back to LibreHardwareMonitor, which is where Windows temperatures actually come from:
# core/hardware_sensors.py
temps = psutil.sensors_temperatures() if hasattr(psutil, "sensors_temperatures") else {}
3. except: pass, and four months of a dead brain
This is the one that still bothers me.
The application learns what is normal for your machine: temperature baselines per workload, voltage anomalies with real statistical process control. Weeks of work. It ran, it was tested, it produced correct numbers.
The chat assistant never called any of it. I found it while grepping for something else and landing on this:
try:
ctx = build_learning_context()
except:
pass
Two lines above, the code referenced a variable that did not exist in that scope. A NameError, every time, on every message, for four months. The bare except swallowed it whole. No crash, no log, no warning. The assistant kept answering. It just answered like an application that had learned nothing, because from its side, it had.
PEP 8 has said the quiet part out loud for years: a bare except clause "can disguise other problems", and you should name the exceptions you actually expect. It is one of the few style rules that is really a correctness rule.
except: pass does not handle an error. It deletes the evidence that one occurred. If you take one line away from this article, take that one. Go and grep your project for except: followed by pass. I will wait.4. The switch wired to nothing
There was a TURBO toggle on the dashboard. You could click it. It animated.
It wrote a flag that nothing in the codebase ever read. The function that was supposed to consume it existed, was correct, and was never called by anything.
Users clicked it, felt productive, and changed exactly zero bytes of system state. I had built the feature, built the button, and never connected them, because I tested the feature by calling the function directly and I tested the button by looking at it.
5. The shortcut that opened nothing
This one reached the Microsoft Store.
Packaged Windows apps live in a folder users cannot right-click into, so the app offers to make a desktop shortcut for you. That shortcut has to carry an Application User Model ID, the identifier the shell uses to tie a shortcut, a process and a window to one application. It is built from the package family name plus the Application Id declared in the manifest.
manifest: Application Id="App"
shortcut code: ...PCWorkman_4hekbcs2ddfbc!PCWorkmanHCK
Every Store user who clicked "create desktop shortcut" got a shortcut that launched nothing. No error dialog. Windows simply found no application by that name and returned to the desktop.
Zero bug reports. Of course zero. Nobody files a ticket about a shortcut that does nothing. They double-click it twice, shrug, and never use it again, and your feature dies without leaving a body.
6. The scan that accused the Print Spooler
There is a process-inspection engine in the app: Authenticode signature checks, typosquat detection, masquerade detection. It correctly catches svch0st.exe pretending to be svchost.exe.
It also flagged spoolsv.exe. The Windows Print Spooler. Valid Microsoft signature, correct System32 path.
The cause is the most instructive one here, and it is not a typo. The process library carries a note meaning "heavy, watch this one for resource use". That note was raising the security verdict. Two entirely different kinds of truth were sharing one field.
Worse, the code already knew better and could not act on it. A valid Microsoft signature is supposed to promote a process to trusted, but that promotion only ran while the verdict was still "unknown", and by the time the signature was read the advisory note had already pushed it to "caution".
The pattern, in one table
Line those six up and they are the same bug in six costumes.
| What actually happened | What the system reported |
|---|---|
| Settings never written | Applied successfully |
| Temperature never read | All clear |
| Learning never called | A confident answer |
| Feature never connected | A working button |
| Shortcut never valid | A shortcut on the desktop |
| Advisory note misread | A security verdict |
Every row is an action that did not happen and a system that said it did. That is the definition worth carrying: a silent failure is a false claim of success. Not an absence of output. A wrong output that happens to be reassuring.
And notice which layer catches none of them. Type checkers do not, because the types are fine. Linters mostly do not, because the syntax is fine. Tests do not, unless someone thought to assert the side effect. The only thing that catches this class is a check that the work happened.
The week I stopped reading carefully
I need to tell you about a specific week, because it is the reason I think this problem is going to get bigger rather than smaller.
I work with an AI assistant and I say so on every post I publish. For most of the last year that meant a conversation: I would ask, read the answer properly, argue with about a third of it, and keep what survived.
Then came a release week. Store submission, a version bump across forty-two files, fifteen new blog posts, a build, a package, and day shifts behind a steering wheel. I started accepting more and reading less. Here is what that produced, in one week.
It filled a database with plausible vendor names
The application ships a library of known processes: name, vendor, what the thing does. It has 521 entries today. To shorten a list of unrecognised processes we added thirty-five of them, and the vendor fields looked entirely reasonable. bash.exe was attributed to "The Git Development Community".
The real Authenticode signer on that binary is a person's name. And because the guard engine compares the expected vendor against the actual signature, a mismatch raises a warning. Three processes that had merely been unrecognised became flagged as suspicious. Confident, plausible, wrong, and worse than writing nothing at all, because "unknown" invites a check and a filled-in field does not.
The fix was to stop supplying vendor names from anywhere except the binary itself.
It wrote a pattern that passed tests and failed in production
A new feature puts clickable links into the chat window. The parser was tested and correct. In the running application, every link rendered as plain text.
# Passed every unit test. Never matched in the running app.
pos = text_widget.search(r'\[-> [^\]]+\]', idx, regexp=True)
The unit tests used Python's re module. The Tk text widget's search does not. With -regexp it hands the pattern to Tcl's own regular expression engine, and in Tcl's advanced regular expressions the backslash stays special inside a bracket expression, which is exactly the opposite of what the Python pattern assumes. Two engines, one string, no error message.
The fix is not a cleverer pattern. It is refusing to write a pattern for an engine I did not check:
pos = text_widget.search('[-> ', idx) # literal, engine-agnostic
m = re.match(r'\[-> ([^\]]+)\]', line_text) # Python parses Python
It ran a destructive command, and a cleanup script ate 38 commas
Cleaning up two local commits, the assistant ran git reset --hard, which also wiped everything uncommitted from that day. It was recoverable only because a full folder backup existed from an hour earlier.
Earlier, a punctuation pass across fifteen HTML files on this very site removed the comma after thirty-eight closing tags. "Driver conflicts, leftover GPU packages" became "Driver conflictsleftover GPU packages". Nothing crashed. Every page rendered perfectly. I caught it only because I diffed the output instead of trusting the exit code.
Notice what none of those did. None threw an exception. None produced a stack trace. Every one produced output that looked right, and two of them produced output that was more confidently wrong than doing nothing would have been.
That is not an argument against working this way. I still work this way, and this project moves faster because of it. It is an argument about where the review has to happen. Generated code is fluent by construction. It compiles, it reads well, it uses the right function names. Fluency is not correctness, and fluency is exactly what makes the difference invisible.
If that makes this article suspect to you, good instinct. Two answers. First, the six failures above are in a public commit history with dates on them, so go and check one. Second, look again at the correction box in section two: this piece changed a claim I had published three times, because writing it forced me to run the check. That is the method working in front of you, not a promise that it always does.
Five checks that actually catch it
None of this is clever. All of it came from being burned.
1. Assert the work happened, not that the outcome looked fine
# Weak: passes when the reading is empty, which IS the bug
def test_no_false_alarm():
assert not monitor.check(temps={}).warned
# Stronger: the reading itself is the subject
def test_temperature_source_returns_data():
reading = sensors.read_cpu_temp()
assert reading is not None
assert 0 < reading < 150
If a test would still pass on a machine where the feature is switched off, it is not testing the feature. For a save button, assert that the file exists on disk afterwards and parses back to what you wrote.
2. Make silence expensive
except Exception as e:
log_event("learning_context_failed", repr(e)) # never silent
ctx = None
One line. The alternative cost four months. If you genuinely want to ignore a failure, ignore a named exception, and leave a comment saying which one and why.
3. Ask which engine actually runs this
The regex passed in the test and failed in the widget because two different engines evaluated the same string. Before trusting a green test, ask whether it exercises the same runtime the user will hit. A test that mocks the boundary you are unsure about is testing your mock.
4. Write the ratchet the same day
When you find one of these, the fix is half the work. The other half is a test that fails the build if it ever comes back. I call them ratchets, because they only turn one way, and I write the reason into the failure message so my future self gets an instruction instead of a red line. Three that live in this repository:
tests/test_navigation.pybuilds the real main window and fires every sidebar item, because a console cleanup once broke every navigation click while the whole suite stayed green.tests/test_shortcuts.pypins the Store identifier that once made the desktop shortcut open nothing.tests/test_version.pyfails the build on any hardcoded version literal, because the version used to live in eight files and had already drifted.
The suite went from 21 tests in June to 331 today. Almost none of that growth came from planning. It came from bugs like the ones above.
5. Click the thing, then check the output rather than the exit code
My most humbling five minutes came after a refactor that split one large module into seven. Every automated test passed. Then I opened the application, clicked a sidebar item, and every page silently fell back to the dashboard. Not one test built the real window.
The same rule applies to any bulk change: the comma script "succeeded", the vendor entries "succeeded", all six bugs at the top of this article "succeeded". After a bulk edit, open the result and count something. Two more habits from the same family, both learned the hard way:
- Instrument the first divergence, not the damage at the end. I spent three days on a replay bug measuring how far apart two runs finished. That number tells you the size of the damage and nothing else. Logging the first tick where the two runs stopped agreeing turned evenings into minutes.
- Trust probes, not names. The app has to detect a read-only install folder to decide where to keep user data. The first version checked whether the path contained
WindowsApps. A name check is a guess about the world. A write probe is a fact about it.
Why one person misses all of this
I build alone. There is no reviewer, nobody to say "hold on, did you check that this actually saved?" The bugs above did not survive because they were subtle. Several were obvious. They survived because exactly one person could have noticed, and that person had already convinced himself the feature worked.
You do not write a test for a feature you already believe works.
That sentence is the whole article, and it is not a technical problem. It is the problem of being the only witness. I am 22, in Radom in Poland, self-taught after a technical school, and twelve projects died before this one. The laptop most of this was built on is from 2014 and reaches 94 degrees under load. You do not do careful review at midnight after a twelve-hour shift. You do the thing that feels finished, and everything in the first half of this article is what "feels finished" looks like six months later.
Three things help, and none of them is discipline.
Write in public. I publish what shipped and what broke every week. Explaining a feature to strangers forces me to describe what it actually does, and the difference between the description and the behaviour is where these bugs live. More than one of the six above was found while writing the post about it, and the correction in section two happened while writing this one.
Ship to people who owe you nothing. Testers found things I could not, not because they are better engineers, but because they had not decided in advance what the software does. One ran it on Windows 11, told me the diagnostic console would not close, and would not accept my explanation that it closes on my machine. He was right. The call was succeeding and doing nothing.
Keep the log. Not for an audience, for the version of you in six months. Everything here came out of a private devlog and a public changelog, and I would have sworn under oath that half of it never happened.
My first one-star review said the interface is too technical and the learning curve is steep. I read it three times and agreed with every word. It taught me more than most of the good weeks.
What the app does about it
All of this leaked into the product, because an optimizer is exactly the kind of software where a silent failure is invisible by design. You click something, a bar animates, a message says done. Nobody measures. So PC Workman now takes a snapshot before the action, another one twenty seconds later, and keeps both:
It is twelve lines of plumbing and it changes the honesty of the whole feature. If an action does nothing, the receipt says so, and I would rather ship that than another green tick. The same rule runs through the rest of the app: the temperature verdict says which workload it compared against, the process scan says "name, path and signature match a known process" instead of "safe", and a reading that came from an estimate rather than a sensor is never allowed into the learning history.
Questions people ask about this
What is a silent failure in software?
A silent failure is a false claim of success. The code runs, returns normally, logs nothing and reports that the work is done, while the work never happened. It is not an absence of output. It is a reassuring output that is wrong, which is why nobody files a bug report about it and why it can survive for months in a shipped product.
Why is a bare except: pass dangerous in Python?
A bare except catches everything, including NameError and AttributeError from your own typos, and pass throws the evidence away. PEP 8 warns that a bare except clause also catches SystemExit and KeyboardInterrupt and can disguise other problems, and recommends naming specific exceptions instead. In a real case in PC Workman, a bare except swallowed a NameError on every single chat message for four months, so an entire learning subsystem was never called and nothing anywhere said so.
How do you test for a bug that already passes every test?
Assert that the work happened, not only that the outcome looked reasonable. Check the side effect: that the file exists on disk after Apply, that a reading has a value in a plausible range rather than an empty container, that the identifier in two files still matches. A useful rule of thumb is that if a test would still pass on a machine where the feature is switched off, it is not testing the feature.
We are getting better at producing code that reads correctly, and faster at producing it. Neither improvement makes code more likely to do what you meant.
So the habit worth building is small and old-fashioned. Do not accept an outcome as proof of an action. Not from your code, not from your tools, not from anything that generates text for you, and not from yourself at midnight.
Look for the receipt.
Sources checked for this article: PEP 8 · psutil API reference · Tcl re_syntax · Tk text search · Microsoft, AppUserModelIDs · Microsoft, Get-AuthenticodeSignature