Skip to main content
CleanCodeMastery

Comments Smell: When Sticky Notes Hide a Messy Cupboard

Learn why too many comments can be a code smell. Understand good WHY comments vs bad WHAT comments with a sticky-note cupboard story and easy examples.

23 min read Updated June 11, 2026beginner
code-smellsdispensablescommentsclean-coderefactoringtypescriptcsharp

📋 The cupboard covered in sticky notes

Meera's house in Pune has one big steel cupboard. It stands in the corner of the hall, grey and heavy, and it has been there since before Meera was born. It is also a complete mess inside. School books are mixed with pickle jars. Winter sweaters sit on top of electricity bills. The sewing kit is hiding inside a shoe box. Papa's old camera is wrapped in a saree that nobody wears anymore.

Meera's mother, Sunita aunty, got tired of searching. Every morning there was the same drama: "Where is the geometry box? Where are the gas bills? Who moved the medicines?" One Sunday, she decided to fix the problem. But instead of arranging the cupboard — which would take a whole afternoon — she did something faster. She took a pad of yellow sticky notes and pasted them all over the doors.

  • "Maths books are behind the pickle jars, third shelf."
  • "Warning: do not move the red box, the bills are under it."
  • "Sweaters are in the white cover, NOT the blue cover. The blue cover has old curtains."
  • "Medicines: bottom shelf, left side, behind the iron."

It worked. For about two weeks.

Then life happened. Papa shifted the sweaters to the blue cover because the white one tore. Nobody updated the note. The pickle jars finished and new ones came on a different shelf. Nobody updated that note either. New notes got pasted on top of old notes. Some notes fell off and got swept away. Soon there were forty sticky notes, and nobody could say which ones were still true.

One day Meera trusted a note, opened the blue cover looking for curtains, and found her missing school project crushed at the bottom — under the sweaters that the note swore were in the white cover. She missed her submission. She was not happy.

Now think about this carefully. Did the sticky notes fix the cupboard? No. The cupboard is still a mess. The notes only describe the mess. And because nobody updates the notes when things move, half of them are lies now. Worse — the notes made the family feel the problem was "handled", so nobody ever felt the push to actually arrange the cupboard.

There was always a better fix: arrange the cupboard properly. Books on one shelf. Clothes on another. Bills in one labelled folder. Then most sticky notes become useless, because the cupboard explains itself.

Code works exactly the same way. When code is messy and confusing, many programmers paste "sticky notes" on it — comments that explain what the confusing code does. The comments do not fix the mess. They only describe it. And like Sunita aunty's notes, they slowly become old and wrong.

This is the Comments code smell. But be careful — this smell has a twist, and we will study it honestly: some sticky notes are genuinely precious, and we will learn to tell the difference.

Figure 1: Meera's day with the sticky-note cupboard

Look at the last section of that journey. The happy scores only appear when the cupboard itself gets arranged — not when more notes get added.

🤔 What is this smell?

Here is the careful definition. Read it twice, because many people get this one wrong.

The Comments smell is when comments are used as a bandage for unclear code. The code is hard to read, so someone writes a sentence above it explaining what it does — instead of fixing the code so the sentence is not needed.

Notice what the definition does not say. It does not say "comments are bad". It says comments that compensate for unclear code are a warning sign. Martin Fowler, in his book Refactoring, calls such comments a deodorant — a sweet spray that hides a bad smell instead of removing it. When you feel the urge to write a comment, Fowler says, first try to refactor the code so that the comment becomes unnecessary.

There are two big families of comments, and the whole lesson lives in the gap between them:

  1. WHAT comments — they explain what the code does. Example: // check if customer gets discount above a long confusing condition. These are the smell. The code should say this itself, through good names.
  2. WHY comments — they explain why the code is like this. Example: // we retry only 3 times because the payment gateway takes 90 seconds to recover. These are gold. No variable name, no method name, no clever refactoring can hold this story. It lives only in a comment or only in someone's head — and heads leave companies.
💡

A simple test for any comment: ask, "Could a better name or a small refactoring make this comment useless?" If yes, refactor and delete the comment. If no — because the comment holds a reason, a piece of history, or a warning — keep it proudly. Good code with a few good WHY comments is the goal, not zero comments.

College corner: This distinction maps neatly onto a concept from software engineering research: code can be self-documenting about its mechanics (what it computes) but can never be self-documenting about its rationale (why this design was chosen over alternatives). That is why mature teams pair clean code with Architecture Decision Records (ADRs) and WHY comments — the rationale layer needs its own home. When you read Robert C. Martin's Clean Code chapter on comments, notice that almost every "good comment" category he lists (legal, intent, warning, clarification of external APIs) is rationale, not mechanics.

Here is the whole territory of this smell in one map:

Figure 2: The full map of the Comments smell

🔍 How to spot it

Use this checklist when you read code. Tick the boxes that match.

  • A comment repeats the next line in English. // increase count by one above count++.
  • A comment names what a block of code does. That block is begging to become a method with that name.
  • A comment translates a long, tangled if condition into a sentence.
  • A comment and the code below it say different things — the comment went stale.
  • The file is full of // TODO, // FIXME, // HACK markers that nobody has touched for months.
  • Big chunks of commented-out code sleep in the file "just in case".
  • A comment apologizes: // sorry, this part is confusing. The code should be un-confused, not apologized for.

If you opened a random old project and sorted its comments into buckets, the picture usually looks something like this:

Figure 3: What you typically find when you audit comments in an old codebase

Only about a quarter of the notes are pulling their weight. The rest are sticky notes on the cupboard — and one in ten is actively lying.

Here is the most important table of this whole post. It shows the difference between a comment that smells and a comment that shines.

CommentTypeVerdictWhy
// add 1 to i above i++WHATBad — deleteThe code already says it
// check loyalty discount eligibility above a 3-line conditionWHATBad — refactorExtract a method named isEligibleForLoyaltyDiscount
// retry 3 times: gateway has a 90-second failover windowWHYGood — keepThe reason lives nowhere else
// do not reorder these fields; serialization breaksWARNINGGood — keepSaves the next person from a hidden trap
/* old code: ... 40 lines ... */DEAD CODEBad — deleteGit remembers it forever
/** Returns the invoice total in rupees, including GST. */ on a public APIDOCSGood — keepTools and users of your library read this
// TODO: fix this someday (2 years old)STALEBad — resolve or deleteA promise nobody keeps is noise
// see RBI circular 2024-17 for this rounding ruleWHYGood — keepLinks code to the real-world rule

And here is a quick decision chart you can hold in your head while reviewing. Where does each comment in your file land?

Figure 4: Sorting comments by usefulness — keep the top-right, refactor the rest

⚠️ Why it is a problem

Why is a WHAT comment so dangerous? Let us count the costs, slowly.

Cost 1: Comments can lie, and nothing stops them. Code is checked by the compiler and by tests. A comment is checked by nobody. When someone edits the code but forgets the comment, the two start disagreeing. The reader now has two stories and cannot tell which one is true. A wrong comment is worse than no comment, because it actively misleads — like Sunita aunty's sticky note pointing to the wrong cover.

Cost 2: Trust dies for ALL comments. The first time a comment fools a reader, the reader stops believing every comment in the project — including the precious WHY comments. One lying note ruins the credibility of forty notes. Meera now ignores even the correct notes on the cupboard, because how would she know which is which?

Cost 3: The pressure to fix the code disappears. A comment explaining messy code makes the mess "survivable". Nobody feels the pain strongly enough to clean it. The mess is preserved forever, with a polite label on it. This is the deodorant effect: the smell is still there, just perfumed.

Cost 4: Double reading. A comment that restates code makes the reader read everything twice — once in English, once in code — and then compare the two versions to check they agree. Extra work, zero new information.

Watch how the betrayal actually happens, step by step, in a real developer's afternoon:

Figure 5: The exact moment a stale comment betrays a developer

And here is the slow-motion life story of a single WHAT comment. Notice that it never jumps from "true" to "lying" in one day — it drifts there silently while everyone is busy:

Figure 6: The life cycle of a WHAT comment — it drifts from true to lying without any alarm

The longer a project lives, the worse this gets. Here is the typical pattern teams report when they track how much of their comment base still tells the truth:

Figure 7: Comments decay over time when nobody maintains them

College corner: There is real empirical backing here. Studies of long-lived codebases (for example, work on "comment-code inconsistency detection") consistently find that comments are updated far less often than the code they describe, and that inconsistent comments correlate with future bugs. The compiler enforces nothing about natural language, so comments are the only part of a source file with no feedback loop. That is the formal reason WHY-comments age better than WHAT-comments: a reason ("the gateway takes 90 seconds to recover") stays true even when the surrounding implementation is rewritten, but a description breaks every time the implementation moves.

Figure 8: How a WHAT comment slowly turns into a trap

Look at the two paths in the diagram. The comment path feels cheaper today, but it ends in lost trust. The refactor path costs ten minutes today and pays back every single day after.

💻 A real-life code example

Let us write Meera's cupboard as code. A small school keeps a store cupboard, and this program manages it. Watch the sticky notes pile up.

// Smelly version: sticky notes everywhere
class StoreCupboard {
  items: { name: string; shelf: number; box: string; qty: number }[] = [];
 
  // this function gives items to a student
  issueItem(name: string, qty: number, studentClass: number): string {
    // find the item in the list
    const item = this.items.find((i) => i.name === name);
    // if item is not there return error
    if (!item) return "Not found";
 
    // check if student is allowed: classes 6 and above can take
    // science items, and quantity must be available, and quantity
    // should not be more than 5 per student as per store rule
    if (
      ((item.box === "science" && studentClass >= 6) ||
        item.box !== "science") &&
      item.qty >= qty &&
      qty <= 5
    ) {
      // reduce the quantity
      item.qty = item.qty - qty;
      // return success
      return "Issued";
    }
    // return failure
    return "Denied";
  }
 
  // old issue function, keeping just in case
  // issueOld(name: string) {
  //   const item = this.items.filter(i => i.name === name)[0];
  //   if (item) { item.qty--; return true; }
  //   return false;
  // }
}

Count the problems:

  1. // find the item in the list — the next line already says find. Pure repetition.
  2. The big comment above the if is doing the job a method name should do. The condition is so tangled that English was needed to decode it.
  3. // reduce the quantity and // return success — kindergarten narration of obvious lines.
  4. The commented-out issueOld is dead code in disguise.
  5. And here is the killer: the comment says "classes 6 and above", but suppose next month the rule changes to "classes 8 and above" and someone edits only the number 6 to 8 in the code. The comment still says 6. A future reader trusts the comment. Boom — wrong understanding, possible bug. That is exactly the betrayal we watched in Figure 5.

🧹 Cleaning it up, step by step

We clean this the way Meera's family should clean the cupboard — by arranging, not by labelling.

Step 1: Delete the narration comments. // find the item in the list, // reduce the quantity, // return success — all gone. The code says these things already.

Step 2: Delete the commented-out code. Git remembers issueOld forever. If anyone ever needs it, git log will find it. This is also the cure described in our Dead Code post.

Step 3: Turn the big WHAT comment into names. This is the star move: Extract Method. The comment "check if student is allowed..." becomes a method canIssue. Inside it, we use Extract Variable to name each small rule.

Step 4: Keep one WHY comment. The "5 per student" rule comes from the store register rule book. That reason deserves a comment, because no variable name can hold it.

// Clean version: the code reads like the comments we deleted
const MAX_QTY_PER_STUDENT = 5; // store rule book, page 12: prevents hoarding
 
class StoreCupboard {
  items: StoreItem[] = [];
 
  issueItem(name: string, qty: number, studentClass: number): IssueResult {
    const item = this.items.find((i) => i.name === name);
    if (!item) return "NotFound";
 
    if (!this.canIssue(item, qty, studentClass)) return "Denied";
 
    item.qty -= qty;
    return "Issued";
  }
 
  private canIssue(item: StoreItem, qty: number, studentClass: number): boolean {
    const allowedAge = item.box !== "science" || studentClass >= 6;
    const inStock = item.qty >= qty;
    const withinLimit = qty <= MAX_QTY_PER_STUDENT;
    return allowedAge && inStock && withinLimit;
  }
}
 
type StoreItem = { name: string; shelf: number; box: string; qty: number };
type IssueResult = "Issued" | "Denied" | "NotFound";

Read canIssue aloud: "allowed age, and in stock, and within limit." It reads like the old comment — but now the compiler checks it, tests cover it, and it can never go stale. The only comment left is the WHY comment on the constant, and that one earns its place.

The structure of the cleaned design looks like this:

Figure 9: The cleaned design — the comment text became a method name the compiler protects

One more handy move: if a comment states an assumption like "balance is never negative here", convert it into a real check with Introduce Assertion. An assertion shouts loudly when the assumption breaks; a comment just sits there quietly while everything burns.

Here is the full menu of conversions, from sticky note to self-explaining code:

Figure 10: The refactoring journey from sticky notes to self-explaining code

🟦 The same smell in C#

The same disease and the same cure, in C#. Before:

// Check if the customer is eligible for the loyalty discount
if (customer.Orders.Count > 12
    && customer.AccountAgeInMonths >= 6
    && !customer.HasOutstandingBalance)
{
    ApplyDiscount(customer);
}

The comment is a sentence trying to become a method name. Let it.

if (IsEligibleForLoyaltyDiscount(customer))
{
    ApplyDiscount(customer);
}
 
private static bool IsEligibleForLoyaltyDiscount(Customer customer) =>
    customer.Orders.Count > 12
    && customer.AccountAgeInMonths >= 6
    && !customer.HasOutstandingBalance;

The comment is gone, but its meaning survived — inside a name that the compiler will keep honest. Meanwhile, a genuine WHY comment in C# stays untouched:

// We retry exactly 3 times because the upstream gateway returns a
// transient 503 during its 90-second failover window; more retries
// only increase user waiting time without improving success.
private const int MaxRetries = 3;

No renaming in the world can explain why the number is 3. That comment is doing a job code cannot do.

And one quick Python flavour, because the smell speaks every language:

# Smelly: narration and a block-label comment
def monthly_fee(student):
    # calculate base fee
    fee = 2000
    # check if student has sibling and apply ten percent discount
    if student.has_sibling:
        fee = fee * 0.9
    return fee
 
# Clean: the names carry the meaning
SIBLING_DISCOUNT = 0.10  # society rule: one discount per family, AGM 2024
 
def monthly_fee(student):
    fee = BASE_FEE
    if student.has_sibling:
        fee *= (1 - SIBLING_DISCOUNT)
    return fee

🏢 Where this smell hides in real projects

In real companies, this smell wears many uniforms:

  • Section-divider comments inside long methods. // ---- validation ----, // ---- calculation ----, // ---- saving ----. Each divider marks a method waiting to be extracted. This is the Long Method smell holding hands with the Comments smell.
  • Graveyards of commented-out code. Old API calls, old SQL queries, old algorithms — kept "just in case" for years. Studies of large codebases regularly find such blocks untouched across dozens of releases.
  • Auto-generated noise. Documentation generators forced on every private method produce gems like /// <summary>Gets the name.</summary> on GetName(). Volume without value.
  • Stale TODO farms. Hundreds of // TODO markers, some older than the interns reading them. A TODO without a ticket and an owner is a wish, not a plan.
  • Apology comments. // I know this is ugly, don't ask. Honest, but the energy spent apologizing could have extracted a method.
  • Tutorial comments in business code. // a for loop iterates over the array — explaining the language instead of the program.

College corner: In code review culture, there is a useful heuristic called "comment-driven refactoring": when a pull request adds a comment above a block, the reviewer asks whether the comment could be the name of an extracted function instead. Kent Beck's idea of intention-revealing names is the theory behind it — the name of a method is a comment that the type system, the IDE's rename tool, and your test suite all keep synchronized for free. A comment has zero enforcement; a name has three layers of it.

⚖️ When it is okay to ignore

This smell is the easiest one to over-correct. Some teams declare "no comments allowed!" — and that is just as wrong as commenting every line. Here is the honest table.

SituationKeep the comment?Reason
WHY explanation (business rule, workaround, tradeoff)Yes, alwaysCode cannot express reasons and history
Public API docs (XML docs, JSDoc, docstrings)YesTools, IDEs, and library users consume them
Legal and license headersYesRequired by law or company policy
Warning about non-obvious consequencesYes"Changing this order breaks serialization" saves someone's weekend
Link to spec, ticket, or regulationYesConnects code to the outside world
Comment narrating an obvious lineNo — deletePure noise
Comment naming a block of codeNo — Extract MethodThe name belongs in code
Commented-out codeNo — deleteVersion control is the archive
Stale comment that disagrees with codeNo — fix or deleteA known lie must never stay
⚠️

Never leave a comment that you know is wrong. A stale comment is a trap armed and waiting for the next reader. The moment you spot one, either correct it or delete it — even if you are in the middle of something else. It takes thirty seconds and saves someone a bad afternoon.

🛠️ Which refactorings cure it

SymptomCuring refactoringWhat it does
Comment names a block of codeExtract MethodThe block becomes a method; the comment becomes its name
Comment decodes a confusing expressionExtract VariableEach sub-rule gets a meaningful boolean name
Comment explains a badly named methodRename MethodFold the explanation into the name itself
Comment states an assumptionIntroduce AssertionThe assumption becomes an executable check
Commented-out codeDelete (trust version control)Git is the museum; the codebase is the living room
Stale or repeated commentDelete or correctTruth must have one source

📦 Quick revision box

+--------------------------------------------------------------+
|  COMMENTS SMELL — QUICK REVISION                             |
+--------------------------------------------------------------+
|  Story    : Sticky notes describing a messy cupboard         |
|             instead of arranging the cupboard.               |
|  Smell    : Comments explaining WHAT unclear code does.      |
|  NOT smell: Comments explaining WHY (reasons, warnings,      |
|             specs, API docs) — these are GOLD. Keep them.    |
|  Danger   : Comments are never compiled or tested,           |
|             so they silently go stale and start lying.       |
|  Test     : "Can a better name make this comment useless?"   |
|             Yes -> refactor & delete. No -> keep proudly.    |
|  Cures    : Extract Method, Extract Variable, Rename         |
|             Method, Introduce Assertion, plain deletion.     |
|  Motto    : Arrange the cupboard. Do not label the mess.     |
+--------------------------------------------------------------+

✏️ Practice exercise

Time to be the cleaner. Here is a small smelly function from a railway ticket program:

// this function calculates fare
function fare(d: number, age: number, w: boolean): number {
  // base fare is distance times 1.5
  let f = d * 1.5;
  // senior citizens above 60 get 40 percent off and
  // children below 5 travel free as per railway rules
  if (age > 60) f = f * 0.6;
  if (age < 5) f = 0;
  // if w is true add 20 rupees
  // (w means window seat which costs extra)
  if (w) f = f + 20;
  // old calculation, do not delete
  // let f = d * 1.4 + 10;
  return f;
}

Your tasks:

  1. Find every WHAT comment and decide its fate: delete it, or turn it into a name.
  2. Rename d, w, and f so the parameters explain themselves. (Hint: the comment about w is begging to become a name like wantsWindowSeat.)
  3. Extract the senior citizen and child rules into a well-named method like applyAgeDiscount. The comment text gives you the name for free.
  4. Delete the commented-out "old calculation". Write one sentence about why this is safe.
  5. Decide: is there any WHY information here that deserves to stay as a comment? (Hint: "as per railway rules" is a reason — could it become a comment linking to the actual rule?)
  6. Draw your own version of Figure 6 — the comment life cycle — for one comment from your own past code. Did it ever reach the "Lying" state?
  7. Finally, write down one comment from your own past code that explains WHAT, and one that explains WHY. Refactor the first. Salute the second.

When your version needs zero comments to be understood — but keeps the one comment that records the railway rule — you have understood this smell completely.

Frequently asked questions

Are all comments bad in programming?
No, not at all. Comments that explain WHY the code does something — a business reason, a workaround for a bug, a tricky decision — are very valuable. The smell is only about comments that explain WHAT the code does, because that usually means the code itself is unclear and should be cleaned instead.
Why is a wrong comment worse than no comment?
Because a comment is never checked by the compiler or by tests. If the code changes and the comment does not, the comment starts lying. A reader who trusts the lying comment will misunderstand the code. After being fooled once, the reader stops trusting every comment in the project.
What should I do instead of writing a comment that explains a block of code?
Use Extract Method. Take the block, move it into a new method, and give the method the name you were about to write in the comment. Now the code reads like a sentence on its own and the comment is not needed.
Is commented-out code also part of this smell?
Yes. Commented-out code is dead code wearing a disguise. Delete it without fear. Version control tools like Git remember every old version, so you can always bring the code back if you truly need it.
When should I keep a comment proudly?
Keep it when it records something the code cannot say: why a magic number is 3, which bug a strange workaround protects against, a link to the rule or ticket behind an odd condition, public API documentation, or legal headers. These comments carry knowledge that lives only in human heads.

Further reading

Related Lessons