My Bad Code Is Your Fault
Not all programming errors are the developer's fault — many are baked into the design of languages, frameworks, and tools that actively lead you into traps. A developer's case against blaming the programmer.
Let's begin with a confession: I write bad code sometimes. I forget to quote a variable in bash. I miss an edge case in a loop. I misname something in a way that misleads the next person. These are my mistakes and I own them.
But there's a category of errors that I refuse to own — errors that are not accidents but consequences. Consequences of tools designed in ways that make mistakes nearly inevitable. When the language defaults to silent failure, when the framework hides a hundred database queries behind a single property access, when the command-line tool has subtly different syntax from every related tool — that's not my fault. That's yours. The fault of whoever designed the thing.
Punishment for Forgetfulness
The classic example is bash variable expansion. Write rm $FOLDER and you get word-splitting. The correct form is rm "$FOLDER". You must remember the quotes every single time. Forget once on a variable containing a space and your command behaves in an entirely different way from what you intended.
This isn't a subtle edge case. It's the default behavior. The shell was designed so that the safe form requires extra syntax and the dangerous form is the natural one.
The same pattern appears in C macros. Define #define SQUARE(x) x*x and call SQUARE(1+2): you get 1+2*1+2 = 5 rather than 9. The fix — #define SQUARE(x) ((x)*(x)) — requires knowing the trap exists. A beginner will be bitten before they learn the rule. The language makes the wrong form syntactically natural.
Bash also defaults to continuing after errors. A script that encounters a failing command proceeds to the next line as if nothing happened. You need to explicitly write set -euo pipefail at the top to get sane behavior. Nobody does this by default. The feature that protects you requires opt-in. The footgun is the default.
Poor Naming
PostgreSQL ships with three default databases: postgres, template0, and template1. What does template0 do? What's the difference from template1? New users either have to look this up or risk accidentally modifying something they shouldn't touch. The names convey nothing useful.
Let's Encrypt puts its challenge files in /.well-known/acme-challenge/. The leading dot on .well-known follows the Unix convention for hidden files. But in a URL, there's no such convention — it's just a directory named .well-known with a dot in the name. Countless developers have been confused when their web server doesn't serve files from what looks like a hidden directory, because they configured their server to skip hidden files.
Python's str.strip() is named to suggest it removes a prefix or suffix. It doesn't. It removes any characters from the provided set, from either end, one at a time. "hello".strip("leh") returns "o" — all the h, e, and l characters are stripped. The behavior is documented but the name actively misleads. The Python team later added removeprefix() and removesuffix() to provide the behavior the original name implies — which implies they knew the original was confusing.
Inconsistent Logic
Bash closes an if block with fi — the keyword reversed. It closes a for block with done, not rof. A case block closes with esac. The rule exists for two of the three constructs and not for the third. There is no reason. It is simply inconsistent.
The default value syntax ${ENV_VAR:-10} uses a dash. A dash in shell usually means something related to stdin or flags. Here it means "default value if unset." The := form assigns the default. The :? form raises an error. The :+ form returns an alternate value if the variable is set. Four different operators, all variations on colons and punctuation, each with distinct semantics, none of them particularly memorable.
Linux ships with both useradd and adduser. They're different programs. One is the low-level system tool, one is a friendlier wrapper. They have different defaults, different flags, different behavior. The naming convention implies they're the same thing with inverted word order.
Hidden Complexity
Go has a custom DNS resolver that, in certain environments, bypasses the system's DNS configuration. Write a Go program, deploy it, and discover it can't resolve hostnames that every other program on the system resolves correctly. The fix involves build tags or environment variables. The behavior is documented — deep in the documentation. Nothing in the surface API signals that this is happening.
Django's ORM makes the N+1 query problem effortless to create. You write:
sources = Source.objects.all()
for source in sources:
print(source.account.name)
This executes one query to fetch all sources, then one additional query per source to fetch its account. One hundred sources: one hundred and one queries. Nothing in the code looks wrong. There's no warning. The ORM silently issues the extra queries behind the source.account property access. You need to know to write select_related('account') — a non-obvious optimization that requires knowing the problem exists before you can solve it.
Deliberate Traps
Bash treats undefined variables as empty strings by default. This enables one of the most famous disasters in scripting:
TARGET_DIR="$1"
rm -rf "$TARGET_DIR/"
Call this script without an argument and TARGET_DIR is an empty string. The command becomes rm -rf "/". The default behavior of the language enables deletion of the entire filesystem. The option to make this an error (set -u) exists but must be explicitly activated.
C and C++ have undefined behavior for signed integer overflow. The language specification says the result is undefined — compilers may generate any output at all. In practice, modern optimizing compilers use undefined behavior as a license to assume overflow never happens, which they use to eliminate safety checks. Code that appears to guard against overflow is silently removed. This isn't a bug in the compiler; it's specified behavior that very experienced programmers routinely get wrong.
SQL's text-based query interface makes injection attacks the natural result of the obvious approach. The straightforward way to include a user-supplied value in a query is string interpolation:
query = f"SELECT * FROM users WHERE name = '{user_input}'"
This is wrong and dangerous, but it's syntactically natural and requires knowing to use parameterized queries instead. The language design rewards the dangerous approach with simplicity and penalizes the safe approach with extra syntax.
Decisions That Cannot Be Undone
Git's interface has been called the most confusing in widespread use. The command git checkout does at least three different things: switch branches, restore files from history, and create new branches. The designers eventually admitted this was wrong and added git switch and git restore — but checkout still exists, still works, and is still what years of documentation, tutorials, and memory muscle point to.
git add -p does not add new files. It stages hunks of already-tracked files. The flag is named "patch" but the behavior — excluding untracked files — is not obvious from the name or from the context of what you're trying to do.
Python's asyncio divided the ecosystem into "async" and "sync" code in a way that cannot be bridged without specific adapter libraries. A synchronous library cannot be called from async code without blocking the event loop. An async library cannot be called from synchronous code without running an event loop. Third-party libraries must choose which world they live in, and that choice propagates to every downstream user. This is called the "red/blue function problem" and it has no clean solution in current Python.
What This Means
I'm not arguing that developers bear no responsibility for their errors. I'm arguing that the distribution of responsibility is wrong when it treats every mistake as developer failure regardless of whether the tool invited the mistake.
When a language defaults to the unsafe behavior and requires explicit opt-in for safety, some proportion of errors caused by that default are the language's fault. When an ORM silently issues a hundred extra queries and provides no warning, some proportion of the performance problems caused by that behavior are the ORM's fault.
The practical conclusion is two-fold. For developers: prefer technologies that default to safety, make the dangerous thing explicit, and fail loudly rather than silently. If you can't avoid a problematic technology, know its traps before they find you.
For tool builders: the design decision that makes the wrong thing easy and the right thing hard will cause errors in proportion to its adoption. That's not the user's fault. That's the design's fault — and therefore yours.