Comments and code
Everyone already knows the basic part: don’t write comments explaining what the code does, because the code already does that. An outdated comment is worse than no comment.
But there’s a set of alternatives that go beyond “use descriptive names” — and that most people don’t consider.
Write the business rule in the code, not above it
When you feel the urge to write a comment explaining a condition, consider whether you can turn that condition into readable code.
// before — the comment carries the context
// legacy plan users created before 2022 don't pay a fee
if (user.plan === 'legacy' && user.createdAt < new Date('2022-02-01')) {
return basePrice;
}
// after — the code carries the context
const LEGACY_PLAN_CUTOFF = new Date('2022-02-01');
const isExemptFromConversionFee =
user.plan === 'legacy' && user.createdAt < LEGACY_PLAN_CUTOFF;
if (isExemptFromConversionFee) {
return basePrice;
}
isExemptFromConversionFee says exactly what’s happening. If the rule changes, the variable name will look wrong — which is better than a silently outdated comment.
Tests are documentation with a guarantee
A well-written test name describes behavior better than any comment — and has an advantage that comments don’t: it fails if the behavior changes.
it('does not charge a conversion fee for legacy plan users created before January 2022', () => {
...
});
This is executable documentation. If someone changes the charging logic without updating the test, CI lets you know. A comment never does.
The “why” lives in the commit, not in the code
Comments explaining decisions — “we chose X instead of Y because of Z” — become outdated along with the code. The right place for that information is the commit message.
fix: use polling instead of a webhook for payment
The provider's webhook endpoint is unstable in the staging environment.
We switched back to polling until support confirms it's resolved.
Ref: #4521
git blame on the line takes you straight to the commit with the full context. A comment in the code doesn’t have that traceability.
A region comment is a sign of a function that’s too large
function processPayment(order) {
// ── validation ─────────────────
...
// ── calculation ────────────────
...
// ── persistence ────────────────
...
}
If you’re using comments to divide sections within a function, the function is too large. Each section is probably a separate function waiting to exist. The region comment is the symptom — the large function is the problem.
Commented-out code isn’t a backup
// const result = oldCalculation(order);
const result = newCalculation(order);
This code shouldn’t exist in any branch. Git history keeps the old version. Commented-out code accumulates, nobody knows whether they can delete it, and it becomes permanent noise in the codebase.
TODO is a promise nobody will keep
// TODO: refactor this later
// TODO: add validation here
// TODO: understand why this number is 42
If it’s important, turn it into a ticket. If it’s not important enough to become a ticket, it probably won’t get done. The TODO stays forever, loses its context, and nobody knows whether it’s still relevant.
When a comment really is necessary
With all that said, there are cases where a comment is the right tool — when none of the alternatives above can carry the context.
Workaround for an external bug:
// Working around a react-pdf@3.1.2 bug that duplicates headers in multi-page tables.
// https://github.com/diegomura/react-pdf/issues/1234
// Remove when upgrading to 3.2.x
const headers = isFirstPage ? columns : [];
A business rule whose origin is external to the code — a legal requirement, contractual agreement, legacy system limitation — where you can’t refactor the context into the code.
Nontrivial mathematics with a reference to the formula or source.
The difference between a necessary comment and an unnecessary one is simple: a necessary comment explains something the code can’t express on its own. An unnecessary one explains the code itself — and that’s the code’s job.