"Clean code" has become a vague phrase that means different things to different people. The useful version is concrete: code is clean when the next person to read it can understand what it does and modify it without surprise. Most "clean code" advice that survives contact with real codebases reduces to that test.
This page is about the principles that actually move the needle, the trade-offs that come up, and the rules-of-thumb that sound good in books but hurt in practice.
Three orthogonal dimensions:
A piece of code that scores well on all three is "clean." Optimizing one at the expense of the others — say, extreme function-extraction for readability that hurts modifiability — is not.
A function named processData is opaque. A function named validateAndStoreOrder says what it does. Naming is the highest-leverage form of documentation; it is the thing every reader sees first.
Specific rules:
validateOrder, not orderValidatorpendingOrders, not dataisReady, hasPermissionurl, id)The "do one thing" rule is correct but often misapplied. The right form: a function should operate at one level of abstraction. A function that orchestrates ("validate, then store, then notify") is one thing at that level. A function that does that orchestration and the validation logic inline is two things.
The test: can you describe what this function does in one sentence without the word "and"? If not, split.
if (status === 3) is opaque. if (status === STATUS_APPROVED) is documenting itself. The constant has a name that explains the meaning.
This applies to URLs, configuration values, error codes, and any other "literal that means something."
When a codebase has a pattern, follow it, even if it is not your preferred pattern. Mixing styles within a file is worse than either style consistently applied. Save style fights for new code, not existing code.
Errors are part of the function's contract. Where errors come from, what they mean, what to do with them — these are first-class design decisions. Burying error handling in a try/catch at the bottom of a function defeats the purpose of typed errors and structured responses.
Sometimes correct, often arbitrary. A 5-line function that consists entirely of well-named single-step calls is fine. A 30-line function that does one coherent thing at one level of abstraction is also fine. The line count is not the right metric.
False at the extremes. Comments that explain why (a hidden constraint, a tricky invariant, a non-obvious design decision) are exactly the comments that age well. Comments that explain what (restating the code in English) usually indicate the code itself could be clearer.
The DRY principle gets misapplied. Extracting a "shared" abstraction from two pieces of code that incidentally look similar but represent different concepts couples them in a way that hurts both. The right test for shared abstraction is shared meaning, not shared form.
Patterns are vocabulary, not goals. Using a Visitor pattern when a switch statement would be clearer is the bad outcome. Apply patterns when the problem they solve actually exists.
// less clean
function approve(order) {
if (order.valid) {
if (!order.locked) {
// ... actual logic
}
}
}
// cleaner
function approve(order) {
if (!order.valid) return;
if (order.locked) return;
// ... actual logic
}
The early-return version is flatter and easier to read.
A function with three boolean parameters has 8 possible callsite combinations, most of them meaningless. Named enums or distinct method names make the valid combinations obvious.
A function that takes inputs and returns outputs without side effects is dramatically easier to test, reason about, and refactor. Where you can write a function as pure, it is almost always better than the side-effecting alternative.
Extract too much and the code becomes a tangle of one-line functions calling other one-line functions. Extract too little and complexity accumulates in single places. The right answer is local: extract when the named function adds clarity, inline when the inline form is plainly visible.
Premature abstraction is more harmful than too-late abstraction. Three duplicated lines is fine. Five very-similar functions probably shares an abstraction. Wait for the third instance to abstract; the first two often turn out to be different in ways that matter.
Validating every input at every level produces noise. Trusting the contract at internal boundaries produces clearer code. The line: validate at trust boundaries (user input, external APIs); trust within the system.