A crash is a gift.
It stops you, it names a file and a line, and it makes the problem your problem immediately, while you still remember what you were doing. Nobody ships a crash to production for six weeks without noticing.
The bugs that survive in a small firm’s software are the ones that make no sound at all. The button that appears to work and does nothing. The document that saves successfully to the wrong place. The calendar sync that stopped in March and has been returning an empty list ever since, cheerfully, on schedule.
Those are not edge cases. In my experience building and running tooling for this practice, silent failure is the dominant failure mode, and every single instance of it shared one property: the software had a perfectly good explanation available and threw it away.
This piece is about how that happens and what to do about it. It is a companion to How Do You Know It Works?, which is about measuring whether an AI workflow produces good output. This one is about the layer underneath — whether the machinery ran at all.
(All examples below are invented composites. No client information, matter numbers, or real data appear anywhere in this article.)
Four shapes it takes
1. The runtime that answers for you.
You build an internal tool as a web app and test it in your browser, where everything works. You wrap it in a native window so it gets a Dock icon. In that wrapper, the built-in confirmation and prompt dialogs are not implemented unless the host application supplies them — so confirm() silently returns false and prompt() silently returns nothing.
Result: every delete confirmation silently declines. Every rename silently cancels. No error is raised, nothing is logged, and the feature is simply inert. The user reports it as “rename isn’t working,” which is exactly right and gives you nothing.
The lesson is not about any particular framework. It is that you tested in a runtime you do not ship.
2. The helpful default.
A function that starts a process accepts a folder to run in, and, sensibly, falls back to a default location if it cannot read the folder it was given. Somewhere upstream, a caller passes a path with an unexpanded ~ in it. The fallback fires. The process runs in the wrong folder.
Nothing errored. Everything “worked.” The output is in a place nobody will look for it.
A fallback is a decision the software makes on your behalf. A silent fallback is a decision it makes and then conceals.
3. The wrapper that eats the explanation.
A small helper program does one job — read the calendar, fetch a record, convert a file. When it fails it writes a clear, human explanation as structured output and exits with a non-zero code, exactly as it should.
The calling code runs it, sees the non-zero exit, and reports the exception it caught:
Command failed: /Users/…/bin/calendar-dump 21 1
The real message — this application does not have permission to read your calendars — was sitting in the helper’s output the whole time, unread, because the exception object and the output stream are different places and only one of them got looked at.
The user is now told that a command failed. They cannot act on that. The system knew precisely what was wrong and could not get it across four inches of code.
4. The permission answered once.
Someone clicks “Don’t Allow” on a system permission prompt — by accident, in a hurry, because it appeared over something else. On macOS, that answer is durable: the operating system records the denial and stops asking. The API does not error. It returns no, forever.
The feature does not announce that it lost a permission. It just returns empty results from that day forward. If your interface renders an empty result the same way it renders “there is nothing scheduled,” you have built a system that says your calendar is clear when it means I cannot see your calendar.
For a litigator, those two sentences are not close to the same.
Why this is a professional problem, not a technical one
Every example above has a legal-practice analogue with real consequences:
- A document generated to the wrong matter folder is a confidentiality problem waiting for the next person who opens that folder.
- A calendar integration silently returning nothing is a docketing system that reports no deadlines, which is indistinguishable from having none.
- A billing capture routine that quietly stops recording is unbilled work you will never reconstruct.
- A rule-verification step that fails open — returning “no problems found” when it could not run — is worse than no verification, because you relied on it.
The common structure: the failed state and the benign state look identical to the user. Empty is empty. Silence reads as “fine.”
That is the design defect. Not the bug — the bug is ordinary. The defect is that the system had no vocabulary for saying I could not do this, so it said the same thing it says when there is nothing to do.
Six rules I now build to
1. Test in the runtime you ship.
If it runs in a wrapper, an app bundle, a scheduled job, or a container, then that is where it gets tested. Three separate bugs in our tooling existed only in the packaged application and never once in the browser used to develop it. Convenience during development is not a testing environment.
2. Every fallback is loud.
If code substitutes a default for something it was given, it must say so — in the response, in the interface, in the log. “I could not use what you gave me, so I used this instead” is a sentence the user must be able to read. Silent substitution is how work ends up in the wrong place.
3. Read the whole failure.
When you shell out to a helper, capture and parse its output before falling back to the exception text. A program that reports failures as data is doing you a favor; do not throw the favor away. This one is a two-line fix that converts an unactionable error into an actionable one.
4. Distinguish “no” from “broken.”
An empty result and a failed request must never render the same way. A calendar with nothing on it says nothing scheduled. A calendar that could not be read says I cannot read your calendar, with a way to fix it. Same for search results, matter lookups, document lists — everywhere.
Corollary, and this one has teeth: when the answer is “you denied permission,” offer the way back. A denial that permanently disables a feature with no in-app recovery is a bug in your product, not a choice by your user.
5. Prove the wiring, then delete the proof.
Where a mechanism can fail silently — a bridge between two runtimes, a scheduled job, an integration handshake — build a temporary self-test that exercises it end to end and reports what it found. Confirm the round trip returns what you expect. Confirm an error propagates as an error. Then remove the scaffolding.
Do this before shipping, because the alternative is a mechanism you assume works, in a category where “does nothing” is the default appearance of both success and failure.
6. Answer the audit question.
For every automated thing in the office, be able to answer: how would I know if this stopped working?
If the answer is “I’d notice,” you do not have a system, you have a hope. Noticing requires a difference, and a silently failing job produces no difference until the day the missing output matters.
Good answers look like: it writes a timestamped record every run and the morning report shows the last one; a failed run posts a message somewhere a person reads; the count of items processed appears where I will see an unexpected zero.
The uncomfortable part
Everything above got worse when I started generating more of this code with AI assistance, and it is worth being precise about why.
It is not that the generated code is bad. It is usually reasonable, and it is frequently more defensive than what I would have written — full of graceful fallbacks and swallowed exceptions and sensible defaults. Which is the problem. Graceful degradation and silent failure are the same code. The difference is entirely in whether the degradation is announced.
Asked to make something robust, a model will reliably produce code that does not crash. Not crashing is not the goal. The goal is that when the thing cannot do its job, a human finds out.
So that has become an explicit instruction in how I ask for this code, and an explicit thing I look for when reviewing it: find every place this silently substitutes, defaults, or swallows, and make it say so. It is a short review, it catches real defects, and it is the highest-yield ten minutes I spend on any of this.
A tool that fails loudly costs you an afternoon. A tool that fails quietly costs you the thing it was supposed to be watching.
Sources
- The two macOS behaviors described above were verified by direct testing on macOS 15, not taken from documentation: (1) after calendar access is denied, an EventKit access request returns denied without re-prompting, and the recorded decision must be cleared before the system will ask again; (2) in an embedded web view, JavaScript
confirm()andprompt()return false and null respectively unless the host application implements the corresponding UI delegate methods. - Both findings are reproducible and were confirmed against the behavior of a working build before being written up here.
This article describes general engineering practice for internal law firm tooling. All examples are invented composites. It contains no client information and is not legal advice.