Skip to main content

Comments

Comments have a strange reputation. Programmers are taught early to comment their code generously, as if comments were self-evidently good. They are not. A comment is what you reach for when the code failed to say what it means on its own, and every comment you write is worth asking whether the code could have said it instead.

This is not an argument for zero comments. It is an argument for treating each one as a cost that has to earn its place, the same way you would treat an extra dependency or an extra layer of indirection.

Comments do not make up for bad code

A common reason to write a comment is guilt: the code underneath is a mess, and a comment feels like an apology or a warning label. It is neither. A paragraph of explanation bolted onto confusing code is strictly worse than spending the same time cleaning the code up, because now there are two things to keep in sync instead of one, and only one of them is checked by ever running.

Clear code with few comments beats cluttered code with many. If you catch yourself writing a comment because the function below it is hard to follow, that is the signal to rename, extract, or restructure, not to annotate.

Explain yourself in code first

Most of what programmers reach for a comment to say can be said by the code itself, usually by extracting a well-named function or variable.

// Check to see if the employee is eligible for full benefits
if (employee.flags & HOURLY_FLAG && employee.age > 65) {
grantFullBenefits(employee);
}
if (employee.isEligibleForFullBenefits()) {
grantFullBenefits(employee);
}

The comment described a boolean expression. Naming that expression removes the need to describe it at all, and it removes the risk of the comment and the condition drifting apart the next time someone edits the flag logic without noticing the sentence above it.

Comments that earn their keep

Not every comment is a failure. A few kinds routinely pay for themselves:

  • Legal notices. Copyright and license headers belong at the top of a file for reasons that have nothing to do with code clarity. Keep them short and point at an external document rather than pasting the whole license in.
  • Informative comments. A comment that spells out what a regex or a return value actually represents - "matches 12:00:00 Mon, Jan 01, 2000" above a date pattern - can be worth it. Still, check first whether the name of the variable or function could carry that same information; a rename is usually the more durable fix.
  • Explanation of intent. Sometimes the "why" behind a decision cannot be inferred from the code no matter how well it is named - why a test hammers the system with thousands of threads, why a comparison intentionally treats one type as always greater. A comment that records the reasoning is worth its weight.
  • Warnings of consequence. A note that a test is disabled because it is slow, or that a class cannot be safely reused across threads, saves the next person a wasted afternoon.
  • TODO markers. A TODO that names a real, currently-blocked piece of follow-up work is useful, as long as the codebase does not let TODOs live forever. Scan for them periodically and delete the ones that no longer matter.
  • Amplification. Occasionally a single line in the middle of a function is far more important than it looks, and a comment calling that out prevents a well-meaning cleanup from quietly deleting it.
  • Clarification for code you cannot change. Comments that translate an unreadable argument or return value are reasonable when the API belongs to a library you do not control. When it is your own API, fix the API instead.

Even here, the bar is "the best comment I could write," not "any comment." A vague explanation of intent is barely better than no explanation at all. This applies doubly to a public API: a well-written docstring is one of the few comments users of your code will actually read, which makes it worth the same scrutiny you'd give any other comment - not an excuse to skip it.

Bad comments, cataloged

Nearly everything else falls into one of these traps.

Mumbling. A comment dropped in because the process expects one, without enough thought to actually explain anything. The reader is left more confused than if nothing were there, because now there is an unanswered question sitting in the code.

Redundant comments. A comment that says exactly what the line below it already says, only slower to read.

// Utility method that returns when this.closed is true.
// Throws an exception if the timeout is reached.
function waitForClose(timeoutMillis) {
if (!this.closed) {
wait(timeoutMillis);
if (!this.closed) {
throw new Error('MockResponseSender could not be closed');
}
}
}
function waitForClose(timeoutMillis) {
if (!this.closed) {
wait(timeoutMillis);
if (!this.closed) {
throw new Error('MockResponseSender could not be closed');
}
}
}

The comment is not just unnecessary, it is actively misleading: it claims the function "returns when closed becomes true," but the real behavior is a timeout-then-throw. A comment that disagrees with its own code is worse than a missing one, because a reader has to trust one of them and might pick wrong.

Mandated comments. Rules like "every function needs a docblock" produce comments that restate parameter names in slightly different words and add nothing. Requiring documentation everywhere guarantees documentation nowhere is worth reading.

Journal comments. A running changelog pasted at the top of a file, one entry per edit, going back years. Source control already does this, and does it better - it does not bloat the file, and it is actually queryable.

Noise comments. Comments that restate the obvious: a /** Default constructor. */ above an empty constructor, a // returns the count above return count. The eye learns to skip these, and once it does, it skips the comments that would have mattered too.

Sometimes noise comments come from frustration rather than habit - a // Give me a break! scrawled next to a nested try/catch that has clearly annoyed the author. The better outlet for that frustration is fixing the structure that provoked it.

try {
doSending();
} catch (SocketException e) {
// normal. someone stopped the request.
} catch (Exception e) {
try {
response.add(makeExceptionString(e));
response.closeAll();
} catch (Exception e1) {
// Give me a break!
}
}
try {
doSending();
} catch (SocketException e) {
// normal. someone stopped the request.
} catch (Exception e) {
addExceptionAndCloseResponse(e);
}
 
function addExceptionAndCloseResponse(e) {
try {
response.add(makeExceptionString(e));
response.closeAll();
} catch (Exception e1) {
}
}

Commented-out code. Few habits age worse than leaving disabled code behind a //. Once it is there, nobody has the confidence to delete it, on the theory that someone left it for a reason. It just accumulates. Source control remembers deleted code perfectly; comment markers do not need to.

Position markers and closing-brace comments. Banner comments like // Actions /////////// and end-of-block markers like } // end while are attempts to compensate for functions that have grown too long to read at a glance. Shorten the function and the marker becomes unnecessary.

HTML, attributions, and nonlocal information. HTML tags inside a comment make it harder to read in the one place comments are supposed to be easy to read: the editor. A byline like /* added by Alex */ is something source control already tracks, more accurately, forever. And a comment that describes some other part of the system - a default value defined elsewhere, a port number owned by a different module - will silently go stale the day that other part changes, since nothing forces the two to move together.

Too much information and inobvious connections. Pasting a spec excerpt or an RFC section into a comment buries the one relevant detail under paragraphs nobody needed. And a comment should make its connection to the code beneath it obvious on sight - if a reader has to study both the comment and the code to figure out what the comment is even referring to, it has failed at the one thing a comment is for.

Don't use a comment when a function or variable will do. This is the single idea most of the above boils down to.

// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (module.getDependSubsystems().includes(subSysModule.getSubSystem())) {
// ...
}
const moduleDependees = module.getDependSubsystems();
const ourSubSystem = subSysModule.getSubSystem();
if (moduleDependees.includes(ourSubSystem)) {
// ...
}

Extracting two well-named variables removes the need to explain what the condition is checking, and unlike the comment, the variable names cannot drift out of sync with the logic - if the logic changes, the names are right there being wrong, not off in a comment nobody re-reads.

The test to apply

Before writing a comment, ask whether the same sentence could instead become a function name, a variable name, or a class name. If yes, write the code instead. If the comment survives that question, it is one of the few that is earning its place, but it should still be treated with more suspicion than the code sitting next to it, since nothing checks a comment for correctness the way a compiler or a test checks code.