Don't write bugs
teamten.com
teamten.com
"We put them there, and we can decide to not put them there"
I don't think this is strictly true. Sure from a strict computational theory or mathematical proof perspective we can 'produce no bugs' but from a meat-space reality standpoint programming ecosystems do not work this way.
The game changes when you need to ship some bits from one place to another, or demand contract on the representation of truths over time, or guarantee no interruption of service on vague notions of "indefinitely".
How perfect can any one individual execute the exacting theory of the stack: ASM(s), C-derivatives, OS Kernels, TCP/UDP, DNS, TLS, pythons/javascripts/golangs, HTTP, SQL-derivatives, libraries, frameworks, cluster orchestrators, oh my the list just keeps getting bigger each and every year...
So we write bugs, because we have to. Imperfect knowledge is part of our professional practice. Much like the sciences our bugs persist not because they were "wrong" but because we build on top of them as new facts emerge.
From the articles principle advice:
"If you want a single piece of advice to reduce your bug count, it’s this: Re-read your code frequently. After writing a few lines of code (3 to 6 lines, a short block within a function), re-read them. That habit will save you more time than any other simple change you can make."
We do this. Programmers can tend to be narcissistic, we love reading our code we just wrote: oh the beauty, oh the wonderful shortcut, oh the performance, all while enjoying the quickly fading context in which we wrote it. If our knowledge was wrong in constructing the code, assumptions are still wrong while reviewing it. But in practice we run code reviews, automated compilation/linters/tests/etc to ensure that the quality of our code is not left to the individual programmer who is quick to forget that it was not for them in the first place. Bugs have a harder time surviving multiple perspectives (generally).
Now back to searching for this years bash incantations :)
However, "Don't write bugs" is a joke for a seasoned programmer who's had to work with political constraints. Bugs often arise from major design flaws that can't be fixed in an afternoon. Other bugs arise from management compromises to meet a deadline or price point. Even worse, some bugs arise from problems with external APIs that are completely out of a programmer's control. At this point, "don't write bugs" can really only translate to trying to defend against corner cases that are impractical to fix.
"Impractical to fix," meaning, fixing the bug requires navigating politics. This could be working with a vendor to fix a broken API. It could also be working with management to allocate time to refactor a defective design.
Also: There are poor programmers who just can't "Don't write bugs." They can't think through edge cases; or a fundamentally incapable of understanding edge cases that lead to bugs. (And handling these kind of programmers is a different topic altogether.)
It becomes a tradeoff between the bug that you think you're smart enough to see but which may never happen, and the bugs that you're not able to see right now.
And I've definitely been on the wrong side of that equation and dealt with overly complicated code considering edge cases that were not useful, which caused complicated but practical bugs which affected many customers -- and I wound up replacing the overly thought out code with simpler code that was understandable and the "bug" I reintroduced never affected anyone AFAIK years later.
I've also been on the right side of that equation and picked simpler approaches and never had the overly complicated edge conditions that I could "see" in the code actually crop up and affect anything.
It isn't just political, sometimes you really can just technically outsmart yourself.
Plus there's the issue of if design decisions that are 10 years old should be refactored at the cost of introducing breaking changes and pain on everyone using your software. Sometimes you wind up writing some less-robust-than-ideal code which is due to those kinds of foundational issues. And the existence of a single difficult to fix bug is generally not sufficient to restort to blowing up your entire world and starting over again.
So, while I understand your point about not "gold plating" code and being stuck solving problems of your own making, there really is a limit to how much code can pretend like failures don't happen and inputs can be other than what is expected. Failing to validate inputs from users is a well-known cause of many security problems, for example.
Failing to validate inputs is fairly trivial and not what I'm talking about.
And I'm not talking about defending my own code as perfect or any kind of egocentric nonsense like that.
I'm talking about reviewing other people's code that I have a difficult time understanding due to a large amount of conditional logic for edge conditions which have never been reported as bugs before. I've accepted that code and then experienced it blowing up really horribly. It also just isn't clear that its better to solve bugs that nobody will ever hit at the cost to clarity and future maintenance and future bugs which people actually hit.
I'd rather people write "buggier" simpler code if the bugs that are mitigated aren't practical. And what I'm talking about here is the "undefined compiler behavior" grey areas of APIs. Those would of course be better never having been created in the first place and everyone should have done excellent design and input validation to start, but that never happens perfectly (because eventually shipping code takes precedence over perfection) and 10 years later you've got a job to do.
And the TL;DR there is that people can actually outsmart themselves trying to think of every edge condition under the sun, and I've watched that happen to other people and had to mop up the fallout.
Sometimes the edge condition will also simply take too long. Mitigating a bug that has been in the codebase for 10 years, has never been reported, and is unlikely to be reported in the next 10 years is not going to be terribly useful if it takes the next 3 months of your time because of how deeply buried in the design the bug is. That is a good one to make a note of and if it starts to surface as a problem you've then had time to think about strategies that might take it down to 1 month or less.
Please do validate all your inputs and write robust code against all the edge conditions you can think of when you're greenfielding though, it will make things easier later. At the same time if you argue that you're shipping flawlessly perfectly designed code I bet that's a lie.
Oh, I've also ripped out 60% of a codebase that was just horribly overdesigned as well, and was solving problems that the author wanted to teach themselves things that weren't actually relevant to the problemspace.
> if you're aware of ways that your code will fail in edge cases, handle those edge cases.
Another key point I want to add is that for every method you write, you should carefully audit the domain of your input and only accept information necessary for your method to work. That way you don't handle edge cases not relevant to your method. The exception probably here is critical edge cases that will SIGSEV, SIGILL, etc and kill your whole process. You usually want to check those - and if you are having to do that frequently that's code smell.
Keeping a log of your mistakes and analyzing them is helpful in any number of things, including coding. Can you not write any bugs? Probably not. But you can certainly reduce the number, and it’s worth taking the time to do so.
The “read what you wrote” advice is good - look for and notice bugs early so they’re easy to fix. Basically be your own pair programmer.
We spend time and money on CI, linting systems, sprint retros — so the allergy to "think more carefully about what you wrote" surprises me.
The first sounds like sensible advice. The second sounds like a completely unrealistic thing that would only be said by someone who doesn't understand fallibility, or humans.
If bugs are business problems, figure out why and deal with the business problems. Maybe that means avoiding bugs but maybe otherwise changing the business makes more sense.
Bug journals, code reviews, prioritizing fixes, tooling support, enough sleep and enough time! If you give me time, then I come up with more and more failure modes, checks, analysis, improved design, better tests. Who pays for that?
Using linters, formatters, and IDE tools have taught me a lot of easily overlooked issues in those languages.
You should say "try really hard not to write bugs" which is what you actually mean.
You can say "try harder to not cut your hand off with the table saw" or you can get a push stick.
An example: imagine I have an API that takes in a users tax id. In the US, let's presume that's an SSN. In Canada, a SIN. In Brazil, a CPF/CNPJ, and so on.
One option is to give a free form field that let's you say country id and tax ID. Simple, works for all countries, extensible later, no problem. Except that okay, in Brazil we also need a tax address. Fine, add an address field as well. But now when a user is using this API to submit Canadian tax numbers, there's a field for tax address. What's that? Do I need it? I guess I'll give the users home address? And a developer trying to submit Brazilian tax IDs sees that the address field is optional, so I guess I don't need that. ("Why didn't they read the docs that said it wasn't in this case?" Because no one does!).
I've made it possible to use this API incorrectly. I did it to save myself effort and blamed the other developer for using it wrong.
On the other hand, if my API had a different input type for each country's tax ID, with required fields saying they are, it becomes impossible to use it wrong. You can't have a bug, a mistake, because the type enforces correct usage. Go even further and have the constructor for the input type for that county explicitly require the fields it must have, and now you're really making it hard to do it wrong.
Types are powerful tools to prevent bugs. A bug caught sooner is less expensive to fix, and how much sooner can you get than "the moment you wrote it"?
Like you just add a new field for every country you support? I can imagine quite a few ways to break that.
For instance, my system has users from USA and CA, and I just send you everything in the SSN field, because I didn't consider the CA users when I wrote it. The simple way would have just worked, but now the API is broken for me.
"But what if I label it incorrectly and send it along?" Then that's a bug in your code. No amount of my API design can prevent bugs in your code. All my API can do is not help you write bugs in your code. I can't stop you from reading the random number generator and claiming its output is a USSocialSecurity number, but at least I've done my best for a reviewer of your code to be able to look at that and ask you what you're doing calling that a USSocialSecurity.
(Social Insurance Number)
Then you've significantly increased the work (and opportunity for bugs) required of a downstream programmer, and also made it significantly more likely that they won't support all countries, and of course they could also still send the wrong type...
Maybe just have a generic "taxblob" fields which is a serialized object which indicates its type and all the properties by name.
https://infiniteundo.com/post/25326999628/falsehoods-program...
Where I worked before we had 100k LoC Elm projects with virtually no bugs.
Now I'm working in a python/django codebase with probably thousands of small hidden bugs.
Overall, strictness and "anal" rules is how you can reduce the amount of common bugs - as long as they don't require too much active thought. Then you can focus on the domain, on the actual problem, and bugs on that level will be from your own limited understanding of said domain and logic, not the programming language, memory management, memory leaks, nil dereferences, etc. Those categories of bugs are solved problems, with either best practices, tooling / linters, analyzers, or newer and better programming languages.
Those are good things, but they only eliminate the class of bugs where the code is wrong. Some bugs, and most important bugs, are problems where you've written entirely valid and working code that does the wrong thing. That is where going back and reading your code again, and having layers of review from your peers, will help most.
You program must break when something is not right because shitty lasts forever.
Immutable objects are great but you need to validate them when you construct them and throw an exception when something is not right. And functions should return what you would expect.
For example, this:
function GetEntitryById(int id):Entity
{
var entity = [action to get an entity];
if(entity == null) {
throw new Exception("Entity not found");
}
return entity;
}
is better than: function GetEntitryById(int id):Entity?
{
var entity = [action to get an entity];
return entity;
}But yeah, better to fail fast. That's why I want tools to catch stuff as I write the code, not when I run it, as I cannot run all cases so then it won't hit until prod.
This is an old timer, back when machines and abstractions were simpler.
The post makes sense in that context, but today you can't have a complete mental model of the thing you're coding. It's impossible. So bugs.
Maybe we need to simplify.
"Back in the day" they would spend weeks devising that system from scratch, hand-writing sorting algorithms, etc.
The mental model of software development is about the problem and domain nowadays, not the low level implementation details. Your job is to solve a business problem, instead of figuring out how to manipulate a CPU and its memory banks to do your bidding.
But those libraries often have parameters, switches, hints, and secret knowledge of the type "do not solve problem X by doing Y because although it seems okay on the surface, the implementational details will increase the complexity from O(N) to O(N^2); use this trick instead".
So we use libraries to solve the problems, but we also need years of experience using those libraries, or we easily create problems we didn't expect. Often there are many alternative libraries for the same task, and the library-specific knowledge becomes obsolete in later version.
If programming worked like the more manual jobs like plumbing then you'd work 5 years on a stack before they'd call you proficient at it. But nowadays you work 2 years before changing jobs to another stack, and after 5 you are expected to manage people and no longer write code. So the problem is mostly organizational and not technical.
So it isn't that the time changed, the old jobs where you code still exists. But we added millions of glue code jobs on top of that, and that changed how people view software development. But it is field specific, even if webdev is the most common you can still work in any of the many other areas where the rules are different.
Edit: And yes, gluing together components is ridiculously hard. It is just hard in a different way, your job then becomes to learn about new things as quickly as possible so you can properly glue them together rather than trying to think about what code to write. That isn't easy at all.
The biggest problem is that many people use libraries as a crutch, without any knowledge of how they work or what they actually do.
Because I bet you fucking money you aren't modeling the actual behavior of any modern x86_64 cpu in your head.
Of course, I also never said x86_64 was a good structure. A bit of reading comprehension might even reveal the implication that simple is better.
Secondly, there's this concept called "Independence Of Testing", which essentially states that the more independent the tester is from the code, the more likely they are to find defects. The opposite of that is also true, the less independend the tester is, e.g. if the tester is the developer, then the less likely they are to find defects in their own code. Read mode here (section 5.1.1): https://www.istqb.org/downloads/send/2-foundation-level-docu...
Lastly, this article is essentially saying that proper testing by QA people is useless, which not only I strongly disagree with but also is something that a terrible developer would say.
But the innovation Java brought is larger than what younger languages bring to the table. You can still dislike the language of course.
I can't stop picking on the 0 compiler errors thing. I could write brainf*ck and get 0 compiler errors, but no guarantees on the buginess of the code. I could also write Rust and the compiler throws errors when an assumption of mine was wrong, or I was trying to do something that the compiler does not yet support, or the codebase was large enough that all of it didn't fit into my mindmap. Not sure how to fully mitigate those causes.
Of course IDE and intellisense goes a long way to prevent compile time errors - it just tells you on-the-fly how not to make a mistake.
Untyped languages could be another story, as "compile-less time" is actual runtime bug. But I don't know - I work primarily with C#.
I take the performance hit and prefer my editor to have syntax highlighting, syntax checking, linters and (at least at some point in my career) relevant unit tests automatically running in the background on save.
Or another perspective, the compiler throwing errors at you for your code implies that your intuitive mental model for the language isn't 100% accurate. If you write code the compiler accepts then you most likely have a near perfect understanding of how the language works.
- Incomplete/ambiguous specification
- Unhandled IO errors
- Unbounded buffers/memory, unhandled OOM errors
- Defects introduced by state mutation
- Defects introduced by concurrency
- Lack of idempotence
- Lack of back-pressure
- Non-determinism
- API changes
- Low performance
Only some of these can "not be written", and only a few can "not be written" in actual code.
Most have to be solved at another layer (the implementation language/tools, the network, the database, the system architecture at large, design documents or team organisation).
School and the software industry are different beasts. Once the pressure of "real life" kicks in, your brain starts to work differently, often trading some of that academic brilliance for other things you need to survive.
Ironically, I have seen some of the best code quality at places with crappy salaries. These companies are aware that they cannot compete with the bigger fish, so they are not as prone to squeeze their developers for that extra cent.
Then you can try with an smoker: Just don't smoke!
Finally you can tell an obese person to loose weight.
They are such high level statements that it is completely useless advice without implementation details.
Neither me or anyone on my team write significant bugs. We spend a very small part of our time dealing with bugs. But for this lots of knowledge, discipline and practice is needed.
You can fill several books just with that knowledge alone.
Rereading your code for me is completely useless. When I was a kid I reread code for days without finding the bug because I just make the same (wrong and most of the time subconscious) assumption over and over again. Then I learn better strategies.
A simple tool that checks for wrong assumptions finds those bugs in seconds.
You can log your bugs so you identify recurring patterns and can correct those patterns.
With just a team of three, I wrote a tool that gave an error for any "one line if" without parenthesis after fixing the nth time the recurring bug: People will write a one liner if without parenthesis and then a year from that will add an additional line to the if without adding a parenthesis because they were just so focused in the new problem they just could not see anything wrong while rereading the indented line.
We even use statistical Bayesian analysis of code that automatically highlights any code that goes against the style of her author. In that code she is not experienced and could make the wrong assumptions so must be careful.
Asserts everywhere in developer code that are automatically eliminated in production code(but remain in developer). We assert everything because bugs just pop up from the asserts again in seconds.
Also incremental tests between revisions is extremely useful. Your test work flawlessly in last revision, you changed 20 lines. It doesn't work, the error is in those lines. The smaller the increment, the easiest to find the problem.
Those things are obvious for experienced programmers, but for other people could be magic and sorcery, because there are many details needed to make it work.
It’s true that you can write code that has no bugs that would be relevant in a programming contest. And that you can avoid the kinds of bugs that you’d find if you read your code 2-3 times.
But if you write code that has to live for a long time, on a lot of devices, used by a lot of people, and that gets attacked by adversaries, then you really start to appreciate the inevitability of bugs even in code that seemed flawless.
Bug-free software can exist, but almost always doesn't because you have to ship, or because you don't have enough staff or time to fix them all, or because the requirements are not perfect, or any number of other perfectly valid reasons.
Bugs exist because it is not possible for any one human or even organization of humans to know all of the ways in which the software is buggy. Worse, in the limit, there's not even a clear distinction between a bug and the absence of a feature.
If your software is in English, then that's a bug for someone who speaks Polish.
If your software has any linear time algorithm, then that's a bug for anyone whose input is large enough to cause that to algorithm to produce an unacceptable delay. What's unacceptable? Depends on the user.
If your software allocates memory, then eventually that memory allocation will fail because there wasn't enough memory, and that will be a bug. It'll be a bug even if your software reports the nicest possible error message. If you say, "sorry I couldn't execute your image filter because I ran out of memory", then that'll be a bug to the poor fellow who needed the image filter to finish executing.
If your software uses color to convey data, then that's a bug for someone who is color blind.
And so on!
And then there are security bugs. Security bugs exist, in part, because it's not possible for any mortal or organization of mortals to predict the entire decision tree that software of any meaningful complexity is executing. There will be some rotting branches in that tree, not because of ship dates or lack of trying, but because code that has enough control flow in it will have too many paths for anyone to reason about. Finding the one path that is broken is way easier than proving that no such path exists.
Some of my favorite bugs are the ones where everyone thought that the OS or CPU or compiler must do one thing, but they simply did not know that the thing that was expected is in no way guaranteed. Sometimes to discover the bug requires extreme acts of brilliance. Spectre is a fun example, and there will be more things like Spectre in the future (and I don't mean more timing side channels -- I mean there will be an entirely new thing, that isn't due to timing and isn't a side channel, that we will learn was wrong with computers all along).
And say that you try to solve this with formal methods. That will require a specification. I guarantee you will get the specification wrong, for all of the same reasons that I think you would have gotten the software wrong. That's not to say that formal methods aren't useful -- they're incredibly useful because they force you to state what the software is doing twice, in different forms, which hopefully enables you to catch bugs by detecting inconsistencies between the two statements about the software (the statement that is the spec an the statement that is the code). But then there will still be a bottomless well of bugs in those areas where the spec and the code both got it wrong. I guess Spectre was a case where everyone's specification of the CPU was wrong.
It's super unhelpful to frame the presence of bugs as an issue caused by the need to ship. That might be true for some bugs, but it misses the point. Bugs creep in any time code gets written. If you need to write code to fix a bug, you'll just introduce another bug by fixing the other one. So if ship dates were pushed back to infinity, then at infinity, you'd still have bugs.
There's a lot of argument over just how many "facts" the average person can keep in their mind at any one time, but let's be extraordinarily generous and say you can keep track of twenty "things", simultaneously.
Now, let's say you need to connect to a URL, download an RSS file, process it, turn it into a list of podcasts, determine which ones haven't already been listened to or downloaded, and download them. Metadata will be persisted in SQLite. Binaries will be written to the file system. You need to do this on MacOS, iOS, Android, Windows, and Linux.
It would be trivial to come up with more than twenty "things" you need to keep track of to do this. The subtle differences between network connections on the various host OSes, the differences in the file system. Are you on SQLite 2 or 3, and what does that mean for the concurrency mechanism? What happens if the user asks you to update the list while you're in the middle of downloading an episode? What do you do when an episode appears in your database, but not the RSS feed? What happens when your download fails? What happens if the user shuts off their phone in the middle of a download?
Software is hard because software is massively, massively complex. There are a thousand things happening under the covers. Every single line of code we write is likely to have side effects, and we usually aren't even going to be sure what those side effects are.
That's why bugs are inevitable. It isn't out of laziness, though lazy coders do write buggier code. It's because we have built a teetering Jenga tower of abstractions, and we keep piling on top of it. This makes us much more productive, in the long run, but it also makes "perfect" code essentially impossible.
My two cents: I can only write average software by writing tests simultaneously or right after writing the functionality. Reading the code thousands times without writing tests would just not work for me.
But debugging has some additional functions. If you interface your standard wep-api, you might want to take a look at the response. Yes, you could do that without a debugger, but I would argue it is more convinient. That expands to anything that is a blackbox to you.
Stopping execution and taking a look at the state of the program is a decent tool I would not want to miss.
In some languages you have an insane amount of dependencies and you need something to evaluate the behavior of those submodules.
For pure algorithmic and determinist programs the statement of Dijekstra is true, but you often don't have it in reality. A server sending you garbled responses might garble you program too.
You also become blind to your own code. You think it does something that isn't reflected in code. You think this regex certainly matches the input, but it just doesn't because you forgot something. Reading again might not help.
That's insufficient in a lot of codebases in a lot of languages. Addition of a few new "locally-correct" lines can break something else in a completely different part of the code.
A trivial example: You've had a correctly-working program, you change a value of a field using correct calculation, and your program becomes incorrect — because other function elsewhere assumed this value won't change.
To avoid this you'd need to re-read the whole program after introducing any change, and that gets exponentially harder.
> In 1976, Hamilton co-founded with Saydean Zeldin a company called Higher Order Software (HOS)[46] to further develop ideas about error prevention and fault tolerance emerging from their experience at MIT working on the Apollo program.
https://en.wikipedia.org/wiki/Margaret_Hamilton_%28scientist...
Crapped on by Dijkstra himself: https://www.cs.utexas.edu/users/EWD/ewd08xx/EWD852.PDF
Written up by James Martin (without proper attribution in my opinion) in "System design from provably correct constructs : the beginnings of true software engineering": https://archive.org/details/systemdesignfrom00mart/mode/2up
See also: https://en.wikipedia.org/wiki/Universal_Systems_Language
Being able to write big bug free chunks means that if I think of something I want to do, then write the 500 line implementation without even compiling or running the code it tend to work the first time I run it. Of course that code isn't production ready, but it helps immensely in writing prototypes and mapping out problem spaces. So when another person does their thing and implement 1 solution, I've written 10 different solutions testing a lot of different architectures and picked the best one.
Edit: I think the biggest win from doing this is that although writing code is the easy and effortless part, debugging your code afterwards is extremely mentally draining and takes away mental energy you could be using to think about architecture etc. Think like this, how much high quality thinking have you wasted on debugging your code? Now imagine if you with a little bit of deliberate practice could eliminate most of that waste, wouldn't you do it?
If I am recalling the story right, he verified each submission in a proof assistant. Eight of the submissions contained errors. These were not first-year, green students writing binary search for the first time.
Empirical studies, few as they are, aren't forgiving either. It turns out that memory management, life times, concurrency, and ordering of computational effects are simply hard for the human mind to comprehend on the scale of non-trivial programs. We need better tools for thinking to help us manage. Otherwise we lean on heuristics and practices and assume some level of tolerance for errors to be made.
Don't write bugs, is not going to be very forgiving when even the most well-trained among us will, eventually, introduce an error into a program.
Furthermore, rarely can commercial products afford the luxury of such exhaustive protocols. Even slow moving, state-funded projects where safety is paramount, and thus - and the devs. basically have years to develop something, without the imminent pressure of showing a fully working product, you'll see bugs appear all over the place.
But I agree with others - there are definitive ways to minimize the problems from the start. Using right tools, enforce the right practices, etc. But with that said, bug-free coding becomes more unfeasible as MLOC, people involved, and new third party tools introduced increases.
In theory, the only bugs that make it in production then are logic or requirements bugs. Having an extra step somewhere of a manual test might help mitigate those.
One logic flaw in that self-reported assessment is that us readers don't really know if his 1000-line programs were truly proven to be bug free. But we can still set that aside and entertain the idea that just deciding to not write bugs actually prevents bugs. Consider various disciplines:
- programming: Some search/sort code that had a hidden bug for 20+ years. Both computer experts Jon Bentley at Carnegie Mellon Uni and Joshua Bloch (Java expert) created the same bug: https://ai.googleblog.com/2006/06/extra-extra-read-all-about...
- mathematics: Andrew Wiles 1st attempt at proving Fermat's Last Theorem had a flaw that was discovered by his peer reviewers. And the authors for a SAT math exam unwittingly wrote a bug: https://youtu.be/kN3AOMrnEUs?t=2m05s
- laws : legislators write laws with attempts at precise wording but it still has unintended loopholes or perverse incentives they never foresaw
Did all those people "decide to write bugs" therefore they can decide not to? Of course not. You can reduce bugs by following some best practices (e.g. use well-tested encryption library instead of rolling your own, etc) but bugs will still happen no matter how much you "decide" against it.
Put another way, conscientious people have already decided to not write bugs but since we don't know how to achieve that objective, it doesn't solve the issue.
public float doubleToFloat(double d) {
return (float) d;
}
1 https://www.teamten.com/lawrence/programming/dont-invent-unn...Written human language is much more forgiving. Just periods and commas are enough most of the time. But with computer languages, it's most of the keys on the \*&#|}|"|{:"?><,./#$(! keyboard.
> Everyone knows good programmers should plan
> for the future, should avoid boilerplate, should
> write elegant abstractions. Don’t. Those “good
> programmers” write heaps of garbage that get
> thrown away.Except maybe one: write a test which somehow exercises those 3 to 6 lines, guided by the cases that occur therein.
Well, yes, with incredibly tiny programs like that, it's possible to be bug free. With real world programs that often run to 100x-1,000x that size, you aren't going to completely avoid bugs just by thinking carefully about what you write.
"I was able to write substantial programs (thousands of lines) and run them without compile or run-time errors on the first try" LMAO :) I believe that :D
The author says in the article that the style site is "unpublished", since the advice is too low-level and outdated. I actually think much of the very basic C advice is still sound and would agree with most of it, but it was kind of jarring when it jumped into "cute tricks", including Duff's device.
I have programmed in C for 25+ years, and have never used Duff's device in anger. While I could have used this guide in 1994 for the basic hints about good C, today I would be very sceptical reviewing code using Duff's device. :)
https://blog.codinghorror.com/falling-into-the-pit-of-succes...
Yes, you can actually focus on not writing any bugs, but should you?
We have limited attention and mental resources.
If you're fixing your car, should you focus on not getting any oil on yourself? Yes, you take precautions but that should not be your main focus.