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.
📋 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.
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:
- WHAT comments — they explain what the code does. Example:
// check if customer gets discountabove a long confusing condition. These are the smell. The code should say this itself, through good names. - 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:
🔍 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 oneabovecount++. - 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
ifcondition into a sentence. - A comment and the code below it say different things — the comment went stale.
- The file is full of
// TODO,// FIXME,// HACKmarkers 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:
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.
| Comment | Type | Verdict | Why |
|---|---|---|---|
// add 1 to i above i++ | WHAT | Bad — delete | The code already says it |
// check loyalty discount eligibility above a 3-line condition | WHAT | Bad — refactor | Extract a method named isEligibleForLoyaltyDiscount |
// retry 3 times: gateway has a 90-second failover window | WHY | Good — keep | The reason lives nowhere else |
// do not reorder these fields; serialization breaks | WARNING | Good — keep | Saves the next person from a hidden trap |
/* old code: ... 40 lines ... */ | DEAD CODE | Bad — delete | Git remembers it forever |
/** Returns the invoice total in rupees, including GST. */ on a public API | DOCS | Good — keep | Tools and users of your library read this |
// TODO: fix this someday (2 years old) | STALE | Bad — resolve or delete | A promise nobody keeps is noise |
// see RBI circular 2024-17 for this rounding rule | WHY | Good — keep | Links 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?
⚠️ 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:
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:
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:
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.
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:
// find the item in the list— the next line already saysfind. Pure repetition.- The big comment above the
ifis doing the job a method name should do. The condition is so tangled that English was needed to decode it. // reduce the quantityand// return success— kindergarten narration of obvious lines.- The commented-out
issueOldis dead code in disguise. - 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
6to8in 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:
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:
🟦 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>onGetName(). Volume without value. - Stale TODO farms. Hundreds of
// TODOmarkers, 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.
| Situation | Keep the comment? | Reason |
|---|---|---|
| WHY explanation (business rule, workaround, tradeoff) | Yes, always | Code cannot express reasons and history |
| Public API docs (XML docs, JSDoc, docstrings) | Yes | Tools, IDEs, and library users consume them |
| Legal and license headers | Yes | Required by law or company policy |
| Warning about non-obvious consequences | Yes | "Changing this order breaks serialization" saves someone's weekend |
| Link to spec, ticket, or regulation | Yes | Connects code to the outside world |
| Comment narrating an obvious line | No — delete | Pure noise |
| Comment naming a block of code | No — Extract Method | The name belongs in code |
| Commented-out code | No — delete | Version control is the archive |
| Stale comment that disagrees with code | No — fix or delete | A 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
| Symptom | Curing refactoring | What it does |
|---|---|---|
| Comment names a block of code | Extract Method | The block becomes a method; the comment becomes its name |
| Comment decodes a confusing expression | Extract Variable | Each sub-rule gets a meaningful boolean name |
| Comment explains a badly named method | Rename Method | Fold the explanation into the name itself |
| Comment states an assumption | Introduce Assertion | The assumption becomes an executable check |
| Commented-out code | Delete (trust version control) | Git is the museum; the codebase is the living room |
| Stale or repeated comment | Delete or correct | Truth 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:
- Find every WHAT comment and decide its fate: delete it, or turn it into a name.
- Rename
d,w, andfso the parameters explain themselves. (Hint: the comment aboutwis begging to become a name likewantsWindowSeat.) - Extract the senior citizen and child rules into a well-named method like
applyAgeDiscount. The comment text gives you the name for free. - Delete the commented-out "old calculation". Write one sentence about why this is safe.
- 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?)
- 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?
- 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
Dead Code: Old Furniture Blocking the Storeroom 'Just in Case'
Learn the Dead Code smell with a storeroom full of unused furniture. See why unreachable code costs real money, with the Knight Capital story and easy fixes.
Long Method: When One Function Tries to Do Everything
Learn the Long Method code smell with simple stories, TypeScript and C# examples, and step-by-step refactoring using Extract Method. Beginner friendly guide.
Duplicate Code: Writing the Same Address on 50 Wedding Cards
Learn the Duplicate Code smell with a wedding card story. Understand DRY, the Rule of Three, and how Extract Method removes dangerous copy-paste code.
Extract Method: Turn One Giant Function into Small Named Helpers
Learn Extract Method step by step. Pull a messy block out of a long function, give it a clear name, and make your code read like a clean to-do list.