29 November 2025 · 3 min
Whenever I say I dislike debugging and organize my programming habits around avoiding it, there is always pushback: “You must not use a good debugger.”
To summarize my view: I want my software to be antifragile (credit to Nassim Taleb for the concept). The longer I work on a codebase, the easier it should become to fix bugs.
Each addition to a pieces of code can be viewed as a stress. If nothing is done, the code gets slightly worse, harder to maintain, more prone to bugs. Thankfully, you can avoid such outcome.
That’s not natural. For most developers lacking deep expertise, as the codebase grows, bugs become harder to fix: you chase symptoms through layers of code, hunt heisenbugs that vanish in the debugger, or fix one bug only to create another. The more code you have, the worse it gets. Such code is fragile. Adding a new feature risks breaking old, seemingly unrelated parts.
In my view, the inability to produce antifragile code explains the extreme power-law distribution in programming: most of the code we rely on daily was written by a tiny fraction of all programmers who have mastered antifragility.
How do you reverse this? How do you ensure that the longer you work on the code, the shallower the bugs become?
There are well-known techniques, and adding lots of tests and checks definitely helps. You can write antifragile code without tests or debug-time checks… but you’ll need something functionally equivalent.
Far-reaching prescriptions (“you must use language X, tool Y, method Z”) are usually cargo-cult nonsense. Copying Linus Torvalds’ tools or swearing style won’t guarantee success. But antifragillity is not a prescription, it is a desired outcome.
Defensive programming itself is uncontroversial—yet it wasn’t common in the 1980s and still isn’t the default for many today.
Of course, a full defensive approach isn’t always applicable or worth the cost.
For example, if I’m vibe-coding a quick web app with more JavaScript than I care to read, I’ll just run it in the browser’s debugger. It works fine. I’m not using that code to control a pacemaker, and I’m not expecting to be woken up at midnight on Christmas to fix it.
If your program is 500 lines and you’ll run it 20 times a year, antifragility isn’t worth pursuing.
Large language models can generate defensive code, but if you’ve never written defensively yourself and you learn to program primarily with AI assistance, your software will probably remain fragile.
You can add code quickly, but the more you add, the bigger your problems become.
That’s the crux of the matter. Writing code was never the hard part—I could write code at 12, and countless 12-year-olds today can write simple games and apps. In the same way, a 12-year-old can build a doghouse with a hammer, nails, and wood. Getting 80% of the way has always been easy.
Scaling complexity without everything collapsing—that’s the hard part.
Daniel Lemire, "Antifragile Programming and Why AI Won’t Steal Your Job," in Daniel Lemire's blog, November 29, 2025, https://lemire.me/blog/2025/11/29/antifragile-programming-and-why-ai-wont-steal-your-job/.
[BibTeX]
I suspect training data just doesn’t contain the same volume of the failure domain discussion as it does of sloppy code. And without understanding potential problems LLMs are just generating checks for things that are easy to check. Countless human written tests do that.
I love the framing of “antifragile” software.
Do you have advices on how to improve at building antifragile codebases? I recognize that this is an abstract concept, but one of the first stage of leanring is imitation.
Good question. More from me in the future.
I like this name, thanks for blogging about it. Some of this stuff is state-of-the-art and done already, other bits strike me as interesting consequences of existing approaches. No doubt Gabriel will explain this better and differently than below.
– Think at the system level, not at the level of individual components.
– Taleb’s book coined ‘anti-fragile’ where the Systems Thinking book by Donella Meadows (and others) says that sustainable flows between parts of a system have to be self-balancing, rather than collapsing-to-zero or exploding-out-of-bounds. A balanced system must recover from imbalanced flows or it must prevent them.
– If you’re doing design or architecture and drawing boxes for the flow of data and/or the flow of logic, *everything* sits above its failure domain. That’s a layer where you’d catch and handle failure (eg exceptions if that’s in your programming enviroment’s paradigm). Always ask ‘how might this fail?’ and invest in recovering from failure.
– Map out the Failure Domains and their recovery paths and use the map to automate recovery where you can. If you can’t fail and can just ‘turn off and back on again’, at least capture some information to improve the recovery pathways.
– SOLID’s helpful if your programming environment is fully object-oriented. If not, find the patterns that help you segregate internal details from public interfaces anyway so that you’re clear about the responsibility boundaries of a given component. That will help you answer “for a given failure mode, what does this component do?”
– Use integration tests to show the limit of how a given component should be used — Hyrum’s Law emerged to tell us that every successful use of an interface will be seen as legitimate and will need ongoing support, something that’s impractical unless you set limits on how a component can be used.
– At every boundary where a component takes responsibility for data, validate whether the inputs are acceptable or not and take the recovery path for ‘this data is no longer acceptable’.
– Use internal or unit tests only this restricted set of allowed data, avoiding the time cost of sustaining 100% coverage of all the data types/traits in your programming environment.
– The books on Site Reliability Engineering say to drill through critical incidents and to maintain playbooks for recovering from previous incidents (emergency responders do both), turning “what is going on?” into “we’ve seen this problem before,” which can go further “…and we’ll learn from this one (and put time in the budget to make changes) to prevent it from happening again.”
– As with static analysis+fuzz-testing, you can also have the CI/CD automate these drills using fault-injection frameworks, which split into ‘triggering a component with bad data’ (should be caught at the interface) and ‘some other fault corrupts trusted data inside your components’ (which may be an expensive rabbit-hole if you’re imagining rowhammer-type or cosmic-ray-induced memory errors, broken disk/cpu/signaling errors and attackers corrupting your system).
K3n.
Cool title but there was nothing inside.
What will you sacrifice to achieve that antifragility. Usually programmers sacrifice code complexity for user-space robustness.
You can’t really introduce redundant code. You can’t not implement a feature for optionality. Most of the time there isn’t antisymmetry.
For your target of not introducing bugs with new patches, it optimises programs into super simple programs that are too small to be useful.
What do you sacrifice? Code complexity or user experience? There is always a trade off.