Technology · PRO

The most dangerous bug
does not crash. It succeeds.

Six failures from fourteen months of shipping alone, all of which ran perfectly and did nothing. Plus the week I let an assistant write more than I read, and one claim in this article that was wrong until I checked it.

By Marcin Firmuga·2026-08-10·13 min read·Technology

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.

On this page
  1. Six real ones, and what each taught me
  2. The pattern, in one table
  3. The week I stopped reading carefully
  4. Five checks that actually catch it
  5. Why one person misses all of this
  6. What the app does about it

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.

A success message is not evidence of success. It is a string. The fix took ten minutes. Finding it took months.

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.

Now the part that only exists in this version of the article. I have been telling this story for months, in a changelog, in a Friday post and in another article on this site, with the same sentence every time: "on Windows it returns an empty dictionary". While writing this piece I finally checked the claim instead of repeating it.

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.

A bare 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.

The two halves of a feature can both be correct and still not be a feature.

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.

Identifiers that must agree across two files will eventually disagree. Test the agreement, not either side of it. There is now one module that builds shortcuts and one test that pins the identifier, with the reason written into the failure message.

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".

When one field carries two kinds of truth, one of them will eventually answer a question it was never asked. A scanner that accuses the Print Spooler is a scanner people mute within a week. A muted scanner protects nothing.

The pattern, in one table

Line those six up and they are the same bug in six costumes.

What actually happenedWhat the system reported
Settings never writtenApplied successfully
Temperature never readAll clear
Learning never calledA confident answer
Feature never connectedA working button
Shortcut never validA shortcut on the desktop
Advisory note misreadA 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:

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:

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:

Optimization Receiptexample
ActionRAM Flush · 87 processes trimmed, 4 protected
BeforeRAM 84% · CPU 12% · CPU temp 61 °C
After 20 sRAM 61% · CPU 9% · CPU temp 59 °C
measuredBoth sides read from the same sensor pipeline. Nothing here is a claim.

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.

Every claim in this article is checkable. PC Workman is free and open source, Windows 10 and 11, with 331 automated tests and a changelog that names the bugs above. Download it or read the source. Related: splitting a 6,533-line file without breaking anything · how to tell a real Windows process from malware.

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

MF

Marcin Firmuga

Solo developer · HCK_Labs · building PC Workman in public

I write about what I actually shipped, with real numbers and real code, including the parts that did nothing. More: my story.