open atlas
↑ Back to track
Code patterns & craft CP · 03 · 01

When comments lie

Comments are not executed, so they rot — and a confidently wrong comment is worse than none. Learn to delete the bad ones and keep the few that earn their place by explaining WHY the code is what it is, never WHAT it does.

CP Middle ◷ 18 min
Level
FoundationsJuniorMiddleSenior

You’re chasing a bug. The comment above the function says // retries up to 3 times. You build your whole mental model on that line — until two hours in you finally read the loop and find it retries five times, because someone bumped the constant a year ago and never touched the comment. The comment didn’t help you. It actively lied to you, and you trusted it because comments look authoritative.

That is the core problem with comments: the compiler never reads them, the tests never check them, and nothing forces them to stay true. Code is the one description of the system that must be correct, because it runs. A comment is a promise no one is enforcing — and an unenforced promise drifts. A confidently wrong comment is worse than no comment at all, because it costs you the time to disprove it.

Goal

After this lesson you can name the four families of bad comment (redundant, outdated/misleading, commented-out code, banner noise) and delete them on sight; state the legitimate jobs a comment actually does — explain why not what, warn of a consequence, encode intent the language can’t, mark a TODO with a ticket; and recognise the failure mode at the other extreme: a “no comments ever” dogma that strips out genuinely necessary rationale.

1

A redundant comment restates the code and adds a second thing to keep in sync. If the comment says exactly what the next line says, it carries no information — it just doubles the surface that can drift.

// increment i by one
i = i + 1;

// set the user's name to the given name
user.name = name;

// loop over all the items
for (const item of items) { /* ... */ }

None of these tell a reader anything the code doesn’t already shout. Worse: when i = i + 1 later becomes i += 2, the comment is now wrong, and you’ve manufactured a lie out of something that was merely useless. The fix is not to write a better restatement — it’s to delete the comment and, if the line was unclear, make the code clearer (a better name, a smaller function). The code is the source of truth; don’t paraphrase it.

2

An outdated comment is a bug with no test to catch it. This is the dangerous family, because it doesn’t just waste a glance — it sends you down the wrong path with full confidence.

// timeout is 30 seconds
const TIMEOUT_MS = 10_000;

// NOTE: assumes the list is already sorted
function median(xs: number[]): number {
  return xs[Math.floor(xs.length / 2)]; // returns the wrong element if xs is NOT sorted
}

The first comment contradicts the literal beside it; a reader who trusts the prose computes with the wrong number. The second is worse — it documents a precondition (already sorted) that nothing enforces, so the function silently returns garbage the day a caller forgets. Comments like these decay precisely because they sit next to code that changes without them. Every comment you keep is a maintenance liability you have signed up to update by hand, forever, with no compiler reminding you.

3

Commented-out code is dead weight — delete it; version control remembers. A block of code “kept just in case”, wrapped in // or /* */, is one of the most common and least defensible comments.

function price(order: Order): number {
  // const tax = subtotal * 0.2;          // old flat rate
  // return subtotal + tax;
  // return subtotal + applyOldDiscount(subtotal);
  return subtotal + taxFor(subtotal, order.region);
}

It answers no question the reader has. Is it the next thing to ship? A fallback? An abandoned experiment? Nobody knows, so everybody leaves it — and it accumulates, rotting alongside the live code, confusing every future search and review. The original is not lost: git log -p and git blame hold every line you ever deleted, with the commit that explains why. Deleting commented-out code loses nothing and removes noise. Keeping it preserves nothing and adds doubt.

4

A good comment explains WHY, not WHAT — the one thing code genuinely can’t say. Code is excellent at expressing what it does and how. What it cannot express is the reasoning that isn’t in the syntax: the constraint you’re working around, the consequence of a “harmless”-looking change, the non-obvious choice you made on purpose.

// Stripe rounds half-up; we must match their cents exactly or the
// reconciliation job flags a 1-cent mismatch and pages on-call.
const cents = Math.round(amount * 100);

// DO NOT parallelise: the upstream API rejects concurrent writes
// to the same account with a 409 we can't retry cleanly.
for (const tx of txs) await post(tx);

// Binary search instead of indexOf: this runs per keystroke on a
// 50k-row list; linear scan dropped input latency below 60fps.
const i = lowerBound(rows, query);

Each of these earns its place. None restates the code; each captures rationale a future maintainer would otherwise have to rediscover by breaking something. The litmus test: if you deleted the comment, could a competent reader recover the information from the code alone? For what/how, yes — so the comment is noise. For why (the 409, the reconciliation, the 60fps budget), no — so the comment is doing real work. Warnings of consequence, encoded intent the type system can’t capture, and // TODO(JIRA-1234): … markers tied to a ticket are all variants of the same legitimate job: recording what the code can’t.

Worked example

Watch a comment turn into a bug, then fix the right thing. A validator ships with a helpful-looking comment:

// allow up to 3 failed login attempts before locking the account
function isLocked(attempts: number): boolean {
  return attempts > MAX_ATTEMPTS;
}

const MAX_ATTEMPTS = 5; // bumped from 3 after the support backlog last quarter

The comment says 3. The code locks after 5. A teammate writing a security report reads the comment, files “accounts lock after 3 attempts”, and the audit is now wrong — not because the code is wrong, but because the comment is. The comment was true once; the constant moved; nothing dragged the prose along with it.

The fix is not to update the comment to say 5. That just resets the same trap for the next change. Delete the lying restatement, and if a number deserves a name and a reason, put the reason — the part code can’t carry — in the comment:

function isLocked(attempts: number): boolean {
  return attempts > MAX_ATTEMPTS;
}

// 5, not 3: raised after Q2 support load showed legit users hitting
// the limit on shared office IPs. See SEC-204 before lowering.
const MAX_ATTEMPTS = 5;

Now the what (the threshold is 5) lives only in the code, where it can’t drift, and the why (the support data, the ticket, the warning against lowering it) lives in the comment, where the code genuinely couldn’t say it. The redundant restatement is gone; the irreplaceable rationale stays. That is the whole discipline.

Why this works

Why is a why-comment defensible when we just spent four steps deleting comments? Because a needed comment is a small, honest admission: the code couldn’t express the intent on its own. The 409 from the upstream API, the reconciliation job that pages on-call, the regulation behind a magic number — none of that lives in any type or name. A comment is the right tool there, but it is never free: it’s still unverified prose you’ve committed to maintaining by hand. So the order of operations matters. First try to make the comment unnecessary — a better name, a smaller function, a type that encodes the precondition. Only when the code genuinely can’t hold the meaning do you reach for a comment, and then you write the part the code can’t say, not the part it already says.

Common mistake

The opposite failure is just as real: a “self-documenting code needs no comments” dogma, applied absolutely, that deletes the why-comments too. Someone removes // DO NOT parallelise: upstream rejects concurrent writes with a 409, sees the loop “obviously” awaits each call for no reason, “cleans it up” into Promise.all, and reintroduces the exact bug the comment existed to prevent. Self-documenting code is the goal for what and how — and the comment was never claiming to document those. It documented a constraint that lives outside the code entirely. “No comments” is a fine reflex against redundant and stale comments; as an absolute rule it strips out the rare, expensive rationale you most needed to keep. Delete comments that restate the code; defend the ones that explain it.

Check yourself
Quiz

You find this on a serial loop over API writes: `// DO NOT parallelise: the upstream API returns a 409 on concurrent writes to one account`. The loop looks like it could trivially be a Promise.all. What should you do?

Recap

Comments aren’t executed, so nothing keeps them true and they rot — a confidently wrong comment is worse than none, because it costs you the time to disprove it. Delete the four bad families on sight: redundant comments that restate the code, outdated/misleading comments that send you down the wrong path, commented-out code (version control already remembers it), and banner noise. Keep only the comments that do the one job code can’t: explain WHY not WHAT — the constraint, the consequence, the non-obvious intent, the TODO tied to a ticket. A needed comment is an honest admission that the code couldn’t express the intent: sometimes unavoidable, never free, always a maintenance liability. And beware the opposite extreme — a “no comments ever” dogma that strips out the rare, irreplaceable rationale you most needed to keep.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.