Comments
The code already says what it does. A comment earns its place by saying why, what constrains it, and what a reader would otherwise reasonably assume and be wrong about.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
What belongs in a comment, given that the code beside it already states what happens?
A reviewer asks why the retry limit is three. The author knows: the provider bills per attempt, and the support contract only allows disputing three charges per transaction. None of that is anywhere in the repository.
Good code is self-documenting. If you need a comment, the code is not clear enough — rename things and extract functions until the comment is unnecessary, then delete it.
It works for the "what", and the "what" is the half that was never the problem. A perfect name tells you the retry limit is three; nothing tells you why three rather than five.
- It works for the "what", and the "what" is the half that was never the problem. A perfect name tells you the retry limit is three; nothing tells you why three rather than five.
- It has no answer for context that has no name: which incident produced this ordering, which regulation forbids the obvious optimisation, which upstream bug this works around and when it can be removed.
- As requirements change, the argument produces a specific and recurring failure — someone deletes a comment that looked redundant, and six months later a well-meaning cleanup removes the code it was protecting, because nothing said the code was load-bearing.
- Taken as a rule it also suppresses the most valuable comment there is: the one that says "this looks removable and is not", which by definition cannot be expressed by making the code clearer.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- A comment is not checked by anything. No compiler, no linter and no test will notice when it stops being true, which caps how much weight it can be asked to carry (Documentation Decay).
- Comments are read in the same diff as the code they annotate, which is their advantage over every other kind of documentation and the reason to prefer them where they fit.
- Some context genuinely lives outside the repository — a regulation, a contract, a supplier's undocumented behaviour — and the only alternative to a comment is a person.
- A comment must be true or absent. A stale comment is not a neutral leftover; it is an assertion a future reader will act on (Documentation Decay).
- A comment must not be the only place an enforceable rule lives. If it can be a type, a test or an assertion, the comment is documentation of a mechanism, not the mechanism.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The comment owns context that has no other home: why, what was rejected, what is externally constrained, and what a reasonable reader would otherwise get wrong.
- The code owns what happens. Any comment that restates it has taken on a responsibility it cannot keep, because it will not be updated when the code is.
- The author of a non-obvious decision owns writing it down at the moment they make it, which is the only moment the reasoning is free to record.
- Inside a function, comments explain local reasoning and are the cheapest documentation there is. On a module's public surface, they are part of the interface and are read by people who will never see the body (Designing a Module Interface).
- A comment on a public API is a contract statement and decays like one. Prefer generating those from types and schemas where the toolchain allows (Documentation Is Part of the Contract).
- Anything longer than a paragraph has outgrown a comment and belongs in a document next to the code, linked from the comment (Docs Close to Code).
The two comments, side by side
The distinction is not subtle once you see it stated. One comment tells the reader something the code already says, and will be wrong as soon as someone edits the code without looking up. The other tells the reader something that is nowhere in the repository and could not be.
Notice what the second one prevents: not confusion, but a specific, plausible, well-intentioned future change.
// retry up to 3 times
const MAX_RETRIES = 3
for (let i = 0; i < MAX_RETRIES; i++) {
// try the request
const res = await send(req)
// if it worked, return
if (res.ok) return res
}// The provider bills per attempt, and our support contract // only allows disputing 3 charges per transaction (contract // 4.2, see BILL-311). Raising this costs real money on every // failing request; it is not a tuning knob. const MAX_RETRIES = 3
The first comment goes wrong the day someone changes the loop, and until then it tells a reader nothing they could not see. The second answers the question a future engineer will actually have — "why not five?" — with a source they can go and check. It is also the comment that stops a plausible performance change from becoming a billing incident.
The comment that stops a cleanup
The highest-value comment in most codebases is the one that says "this looks wrong and is deliberate". It cannot be replaced by better naming, because the whole problem is that the code looks like something a reasonable person would tidy up.
Give it a source and a removal condition, and it stops being folklore: someone can check whether the reason still holds, which is the only way a workaround ever gets deleted.
1// Deliberately sequential. The provider rate-limits per2// connection, not per account, so Promise.all here produces3// 429s under load - see incident 2024-11-08.4//5// Remove when upstream ships connection pooling:6// github.com/vendor/sdk/issues/44127for (const id of ids) {8 results.push(await client.fetch(id))9}Three things make this work: the reason, the evidence a reader can verify, and the condition under which it should be deleted. Without the last line this becomes permanent, because nobody will ever feel confident enough to remove it (Revisit Triggers).
When a comment is the smell
A comment can be a legitimate finding in review, but only as a question. There is a case where a restating comment is exactly right, and treating the smell as a verdict is how teams end up deleting the useful ones along with the noise.
looks like A line-by-line narration — // increment the counter, // loop over users — or a doc block that lists the parameters again with no added information.
suggests Either that the comment is pure noise, or that the code beneath it is unclear enough that the author felt they had to translate it — in which case the naming or the structure is the real finding (Naming).
fix If the code is clear, delete the comment in the same commit as any change to that code. If the comment exists because the code is opaque, fix the code and let the comment go with it. If the code is irreducibly dense, keep the restatement and add the why.
How to build it
Most important first.
- Write down why, especially why-not: the alternative that was tried and failed is the single highest-value sentence in most codebases, because it prevents the annual re-proposal (Decision Records).
- Record external constraints with their source — a ticket number, a contract clause, a link to the upstream issue — so a reader can check whether the constraint still holds.
- Warn about non-obvious context: "this loop is deliberately not parallel, the provider rate-limits per connection". That is the comment that stops a future optimisation from becoming an incident.
- Give every workaround a removal condition.
// remove when upstream #4412 shipsturns a permanent mystery into a task with a trigger (Revisit Triggers). - Delete comments that restate code, in the same commit as any change to that code — they are the ones that go wrong first and mislead most.
- Prefer an executable form when one exists: a named constant, an assertion, a test with a descriptive name. A comment is the fallback for what none of those can express (Naming).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- With the "why" recorded, the next change costs a read: someone considering removing the retry limit sees the billing constraint and stops. Without it, the same change costs an incident, or a week of archaeology, or both.
- The comment that is a restatement makes the next change more expensive, not less: every edit to the code is now two edits, and the second one is optional in a way that nothing enforces.
- The recurring cost this avoids is the re-proposal. A rejected alternative that is not written down is proposed again roughly annually, and each round costs a meeting plus a prototype (Decision Records).
- Comments are unverifiable, so every one you write is a maintenance obligation with no enforcement — and the honest response is to write fewer and better ones, not more.
- Insisting on why-comments slows down authors who genuinely do not know why yet, and produces plausible-sounding rationalisations if the standard is applied mechanically.
- A comment is invisible to everyone outside the repository: support, product and on-call will not find it, which is exactly when the "keep it close to the code" instinct works against you (Docs Close to Code).
What can go wrong
- The comment says what the code used to do. A reader trusts it, and the trust is the damage — without the comment they would have read the code (Documentation Decay).
- Commented-out code accumulates. It has no owner, no test and no reason, and version control already has it.
- Every function acquires a template doc block full of restated parameter names, so the signal-to-noise ratio collapses and reviewers stop reading any of them.
// TODOwith no owner and no date, which is a wish rather than a plan and outlives everyone who could act on it (Deliberate Debt).
- A comment depends on the code beneath it staying the same, with nothing to detect when it does not. That is the entire fragility, and it is why proximity matters more here than anywhere else.
- Comments referencing external systems depend on those systems: an upstream bug fixed two years ago leaves a workaround with a comment that is now wrong about the world rather than about the code.
- Nothing depends on a comment, which is why removing one is unreviewable and why a load-bearing comment should say that it is.
- "Comments are a failure of the code." An overstatement. It is true of comments that restate behaviour and false of everything else — a billing constraint, a rejected alternative and an upstream bug have no expression as a name, however good the naming is.
- "So comment everything important." Volume destroys the signal: a file where every line has a comment is a file where nobody reads any of them, including the one that matters.
- "Comments do not affect design." They do, backwards: the difficulty of writing an honest comment for a unit is a reliable signal that the unit does several things (Single Responsibility, Carefully).
- "Delete all comments and rely on git." Version control preserves the code, not the reasoning, and a
git blameleading to "fix bug" is not a why.
- duplicate-knowledge
Testing it, and how it ages
- A test with a descriptive name is a comment that fails when it becomes untrue, which is strictly better wherever the fact is expressible as behaviour (Testing as Design Feedback).
- Executable examples in doc comments — doctests, compiled snippets in CI — are the only comments with a decay detector, and are worth the setup on a public API.
- Nothing tests a "why" comment. That is inherent: it is a claim about the world outside the process, and review is the only check available.
- Why-comments age well because they are claims about a moment: "in 2024 the provider billed per attempt" stays true even after the provider changes, as long as it is dated.
- What-comments age badly and predictably, which is the practical argument behind "self-documenting code" even though the slogan overshoots.
- A comment that grows past a paragraph is asking to become a document, and one that keeps growing is usually describing a design that deserves an ADR (Architecture Decision Records).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat code can express what happens but not why it was chosen is a property of what code is, so the split holds in every language — only the mechanisms for expressing the "what" more clearly differ.
- LANGUAGE-SPECIFICIn Rust, Go and Java a doc comment on a public item is extracted into reference documentation and, in Rust, its examples are compiled and run — so a public doc comment there is closer to a tested contract than to a note, and deserves correspondingly more care than a comment inside a private function.
- CONTESTEDThe strongest form of the anti-comment position: comments are unverified assertions that decay silently, so a codebase with fewer of them is more trustworthy, and almost everything people write as a why-comment could be a test name, a named constant, or a decision record with a date. Practitioners holding this line point out that "explain why" is used to justify the restating comments too, because everyone believes their own comment is the valuable kind. The answer is not that they are wrong about the risk — it is that some constraints genuinely have no executable form, and pretending otherwise loses the information entirely.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — some languages extract, type-check and execute doc comments as part of the build, which is the only mechanism that turns a comment into something with a decay detector.