Comments that lie and comments that help
In short: A good comment says why the code is the way it is; a comment that retells the code goes stale and starts to lie, so it is replaced with a good name or a function.
Problem
The comment everyone trusts
The shop gives free delivery above a certain amount. Last spring the threshold was €50, and the comment above DeliveryFee says exactly that. Then marketing raised it to €100. The number in the code was changed. The comment was not.
This fall a colleague writes the banner in the cart. They read the comment, not the code, and put 50:
// Free delivery for orders of €50 or more.static decimal DeliveryFee(decimal total){ if (total >= 100) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= 50) { return "Delivery is free!"; } return $"€{50 - total:F2} more for free delivery";}decimal total = 60;Console.WriteLine($"Cart: €{total:F2}");Console.WriteLine(Banner(total));Console.WriteLine($"Delivery: €{DeliveryFee(total):F2}");Cart: €60.00Delivery is free!Delivery: €4.90A customer with a €60 cart reads "Delivery is free!" and then pays €4.90 for delivery. Support looks at DeliveryFee and finds no bug: the function does exactly what its code says. The bug is in text that nobody runs.
See it
The code moved on, the comment stayed
Here is what happened, step by step:
// Free delivery for orders of €50 or more.static decimal DeliveryFee(decimal total){ if (total >= 100) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= 50) { return "Delivery is free!"; } return $"€{50 - total:F2} more for free delivery";}
decimal total = 60;Console.WriteLine($"Cart: €{total:F2}");Console.WriteLine(Banner(total));Console.WriteLine($"Delivery: €{DeliveryFee(total):F2}");Step by step, in words
- The first version: the comment (line 1) and the check (line 4) say the same thing: €50.
- The threshold becomes €100. Someone changes the number on line 4 and leaves the comment. The compiler checks code. Nobody checks comments.
- A colleague writes the banner. They read the comment, not the code, and put 50 on line 13.
- A cart of €60. The banner asks its own threshold: 60 ≥ 50, so "Delivery is free!"
- The fee asks the real threshold: 60 < 100, so delivery costs €4.90.
- The customer reads "free" and pays €4.90. The lie is on line 1. The bug it caused is on line 13.
The compiler checks every line of code. Tests check what the code does. Nobody checks the comment: it is text next to the code, and every change to the code can leave it behind. This is called a stale comment. It is worse than no comment, because it sounds sure of itself.
The idea
Let the code say it
The threshold is a shop rule, and the rule lives in two places: in the fee code and in the head of everyone who read the comment. Give it a name, and both functions ask the name:
const decimal FreeDeliveryFrom = 100;
static decimal DeliveryFee(decimal total){ if (total >= FreeDeliveryFrom) { return 0; } return 4.90m;}
static string Banner(decimal total){ if (total >= FreeDeliveryFrom) { return "Delivery is free!"; } return $"€{FreeDeliveryFrom - total:F2} more for free delivery";}decimal total = 60;Console.WriteLine($"Cart: €{total:F2}");Console.WriteLine(Banner(total));Console.WriteLine($"Delivery: €{DeliveryFee(total):F2}");Cart: €60.00€40.00 more for free deliveryDelivery: €4.90The comment is no longer needed: the name says the same thing. The difference is that a name cannot fall behind. If the threshold becomes €150, you change one line and Banner gets the new threshold. If someone deletes the constant, the program stops compiling.
This is the rule from the lesson on names (Names in code): before you write a comment, check whether a name can say the same thing.
Try it
Predict the output
A hotel sells nights online. What does the program print?
// The price with 20% VAT.static decimal WithVat(decimal net){ return net * 1.09m;}
Console.WriteLine($"€{WithVat(100):F2}");The comment says 20%, and the code multiplies by 1.09. Hotel nights in Bulgaria have 9% VAT, and someone changed the number without touching the comment. The program listens to the code.
See it
Comments that repeat the code
Every comment here says the same thing as the line below it:
// The price of the product.decimal price = 25.00m;// The quantity.int quantity = 3;// Multiply the price by the quantity.decimal total = price * quantity;// Print the total.Console.WriteLine($"Total: €{total:F2}");Total: €75.00You read everything twice and learn nothing new. Worse, you learn to skip comments, and you skip the one that matters too. Delete them. The program is the same, and so is the output.
See it
A comment instead of a name, a comment instead of a function
Here the comments explain things the code hides:
int n = 7; // number of orders of the customerdecimal s = 420; // what the customer spent, in euros// The customer is VIP: at least 5 orders or at least €500 spent.if (n >= 5 || s >= 500){ Console.WriteLine("VIP discount: 10%");}VIP discount: 10%The comment next to n is there because the name says nothing. Give it the name from the comment: orders. The comment above the if describes a rule. A rule with a name is a function:
static bool IsVip(int orders, decimal spent){ return orders >= 5 || spent >= 500;}int orders = 7;decimal spent = 420;if (IsVip(orders, spent)){ Console.WriteLine("VIP discount: 10%");}VIP discount: 10%The output is the same, and there are no comments. The if line reads like a sentence. If the rule becomes "at least 10 orders", you change the body of IsVip and every place that calls it gets the new rule. How to pull out a function like this is in the lesson Functions.
See it
Commented-out code and a TODO without an owner
static void Confirm(string orderId){ SendEmail(orderId); // SendSms(orderId); // if (smsEnabled) // { // SendSms(orderId); // } // TODO: fix}Email for order #1063Is an SMS sent? Nobody knows if it was turned off on purpose, for a while, or by mistake, and nobody dares to delete it. That is how commented-out code lives for years. Delete it: the old code is in the git history and comes back with one command.
A TODO without a name and without a date is a wish nobody owns. A good TODO says who, which ticket and when:
static void Confirm(string orderId){ SendEmail(orderId); // TODO(maria, SHOP-412): add SMS once the new provider passes its tests.}Email for order #1063Better still, the task lives in the ticket, and only the link stays in the code.
The idea
What a good comment says
The code says what it does. A good comment says something the code cannot say:
// The courier gets 30% of the fee, rounded down to the cent.// That is what the contract says (clause 4.2), so there is no Math.Round here.static long CourierShareCents(long feeCents){ return feeCents * 30 / 100;}long fee = 495; // SHOP-388: the courier sends the fee in cents, not in euros.Console.WriteLine($"Courier: €{CourierShareCents(fee) / 100m:F2}");Courier: €1.48There are three such things here:
- why: rounding down looks like a bug until you read that the contract says so. Without the comment, someone will "fix" it;
- where to learn more: ticket
SHOP-388tells the story that does not fit in the code; - the unit that the type does not say: cents, not euros. In the lesson on code smells you will see how this moves into the type itself.
One more: a warning like "Slow: every call is a request to the bank, about 300 ms. Do not call it in a loop." A comment like that saves the next person a day of searching.
A good comment can go stale too. But it rarely does, because it does not repeat the code: the contract does not change every time you change the function.
More precisely, if you are curious
git blame shows when each line was written. If the code below a comment is newer than the comment itself, be careful: someone touched the code and may not have reached the comment.
See it
Documentation for the caller
Whoever calls DeliveryFee from another module does not want to read its body. They want to know what to pass and what they get back. For this every language has a documentation comment, which the editor shows when you hover over the name:
const decimal FreeDeliveryFrom = 100;
/// <summary>The delivery fee for an order.</summary>/// <param name="total">The total of the goods in euros, without delivery.</param>/// <returns>0 from FreeDeliveryFrom up, otherwise the courier fee.</returns>static decimal DeliveryFee(decimal total){ if (total >= FreeDeliveryFrom) { return 0; } return 4.90m;}Console.WriteLine($"Delivery for €60: €{DeliveryFee(60):F2}");Console.WriteLine($"Delivery for €120: €{DeliveryFee(120):F2}");Delivery for €60: €4.90Delivery for €120: €0.00/// above the method, with <summary>, <param> for each parameter and <returns> for the result. Visual Studio and Rider show them at every call, and the compiler can collect them into an XML file next to the program.Documentation states the contract: what the parameter means and in which units, what comes back and when there is an error. It does not say how it works inside. The numbers from the code are not repeated in it: instead of "from €100" it names the constant, so it cannot fall behind.
More precisely, if you are curious
<GenerateDocumentationFile>true</GenerateDocumentationFile> in the project, the compiler warns about a public method without documentation (CS1591) and about a <param> naming a parameter that does not exist (CS1572). So at least the names in the documentation cannot go stale quietly.Fill in the missing part:
const decimal FreeDeliveryFrom = 100;
/// <summary>The delivery fee for an order.</summary>
/// < name="total">The total of the goods in euros, without delivery.</param>
/// <returns>0 from FreeDeliveryFrom up, otherwise the courier fee.</returns>
static decimal DeliveryFee(decimal total)
{
if (total >= FreeDeliveryFrom)
{
return 0;
}
return 4.90m;
}<param name="total"> describes the parameter total.Try it
Find the bug
The shop sends a "How was your order?" email a day after delivery. The customer's latest order is from Oct 24, and the email asks about the one from Oct 12:
var orders = new List<string> { "2026-10-19 #1057", "2026-10-24 #1063", "2026-10-12 #1041" };Console.WriteLine(Reminder(orders));How was your order 2026-10-12 #1041?Which line takes the wrong order?
Pick the line with the bug.
This line takes the first order, because the comment says "from the newest to the oldest". The code sorts by date going up: the first one is the oldest. Someone changed the order for the monthly report and left the comment. The fix has two steps: take the last order, and replace the comment with a name that says the order:
static List<string> OldestFirst(List<string> orders){ var sorted = new List<string>(orders); sorted.Sort(); return sorted;}
static string Reminder(List<string> orders){ string latest = OldestFirst(orders)[^1]; return "How was your order " + latest + "?";}var orders = new List<string> { "2026-10-19 #1057", "2026-10-24 #1063", "2026-10-12 #1041" };Console.WriteLine(Reminder(orders));How was your order 2026-10-24 #1063?Exercise
Clean up the comments
Solve it in your own editor.
Discount for loyal customers. The function has a comment that lies, a comment that repeats the code, a commented-out line and a TODO without an owner:
// Discount for loyal customers: 5%.static decimal LoyaltyTotal(decimal total, int orders){ // if the customer has more than 10 orders if (orders >= 20) { // total = total * 0.95m; total = total * 0.90m; // reduce total } // TODO: rounding return total;}Console.WriteLine($"Sum: €{LoyaltyTotal(200, 25):F2}");Console.WriteLine($"Sum: €{LoyaltyTotal(200, 12):F2}");Sum: €180.00Sum: €200.00Clean it up: remove the noise and the dead code, replace the numbers with names, and either do the TODO or give it an owner and a ticket. The output must stay the same.
More tasks
- Document the order. Write a documentation comment for
OldestFirstfrom the orders task: what it takes, what it returns and in which order. - The banner at €99.99. Run the banner from the card "Let the code say it" with a cart of €99.99 and of €100. Check that both functions agree right at the threshold.
- Your project. Open your latest project and find three comments. For each one decide: does it repeat the code, say why, or lie?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: static: what all objects share · Lists · Names in code · Functions: one job, few parameters, no hidden side effects
- Continues in: Code smells: long method, large class, duplication, primitives instead of types, long if chains · Exercise: a messy class, cleaned up step by step
- Tasks on this topic: Clean code and refactoring