The Myth About Code Comments

February 28, 2024

Code comments can be a divisive topic in software engineering. Where to use them, when to use them, how to structure them, and how much detail should they have? These are the wrong questions: instead we should be asking ourselves, “should I even be writing a comment at all?”

Software engineering has a pervasive myth, common even among those who don’t write too many comments. It goes something like, “Comments are a good way to provide additional context and understanding to a program.” This is wrong and needs to be purged at all costs.

The reality today is that most software engineers write code in high-level languages that have names and structures that can clearly communicate the intent of the author. We have variables, functions, classes, design patterns, and everything else that will show how the program works by its design, rather than external documentation. We are no longer trapped in the days of C and Assembly where the programming language is so close to the hardware of the machine that additional context is required to understand the program.

A code comment you find today in a higher-level language, such as JavaScript or Python, is more likely to be noisy or redundant. Or even worse, harmful.

But…But…

Look, I’m not telling you to never write comments. I’m telling you that comments can be harmful, and you should use code instead. We can all think of cases where comments are still useful, although they are few and far between. Here are a few examples:

  1. Copyright and author comments
  2. Describing a wonky syntax or external API that is simply confusing and unintuitive
  3. Public APIs

A decade ago, Internet Explorer 8 was torturing web developers. The simple CSS property “opacity”, which we take for granted today, did not work in this browser. If you wanted to add 25% opacity and for it to be compliant to all browsers, you had add the following properties:

.translucent { opacity: 0.25; -ms-filter: "progid: DXImageTransform.Microsoft.Alpha(Opacity=25)"; }

Behold the absurdity of this. Because of how weird this is, it would not offend me if a teammate added a comment here to explain this arcane Microsoft nonsense. As developers, we have all worked with all kinds of packages, libraries, and platforms that have gone way out of bounds that we needed to smooth over. In these cases, code can fail to communicate – because our code is built on other code that has already failed.

Public APIs and packages that have comments can sometimes be extremely helpful. I used a framework called Symfony for years, and their source code was documented via comments. If I wanted to know the low-level details of a class or function, I could usually dig into the source code and find a comment block that described it in more detail. Symfony’s high-level documentation is extremely good, and having some notes along with the source code was really useful for curious users of their framework. The comments were meticulously maintained, which is a burden that I don’t think any product team should carry. However, if you are maintaining an open-source package with millions of installs, then this can be a helpful addition for your users. The main takeaway though is that this is not the default. You shouldn’t be adding documentation as comments because you think it’s “best practice.” You should do it because you believe the engineering burden is valuable to your downstream users.

The Truth

Now that we have the excuses out of the way, let’s focus on the truth. If you’re working on a project, and you really need to know how something works, what do you do? Do you read documentation? Do you consult design artifacts? Do you check the Notion and/or Confluence? Hell no. You read the code. Code don’t lie. The only way, the surefire way, to understand how a program works is to read the code.

All other pieces of information cannot be verified like code can. They fall out of date. They rot. Software changes, but documentation doesn’t. How many times have you read a README.md only to find that it was years out of date, forcing you to read the code yourself to understand how to bootstrap a project? How many times have you read documents only to find they sit on a throne of lies?

The truth is that comments fall into the same category. At the very least, most comments are just noise. Time wasters. At the worst, comments are downright harmful, explaining to you outdated information that will poison your mind with incorrect assumptions.

Code Don’t Lie 🏀

Today, web developers get to work with high-level languages that have all kinds of features. Our code can be expressive by using variables, function signatures, classes, and design patterns. We use powerful IDEs that have static analysis and indexing. Code can be perfectly readable and understandable simply by using the programming language itself. We are no longer working with C or Assembly where code is hard to read, and are forced to manage low-level details like memory addresses. When developers say that self-documenting code includes comments, I always cringe. Self-documenting code is clean code that can be understood by reading the code itself.

So if our goal is to write code that communicates what it does, it should not include adding additional fluff to give context. Our code should include that context by how the code is written.

“Clean code reads like well-written prose. Clean code never obscures the designer's intent but rather is full of crisp abstractions and straightforward lines of control.”

— Grady Booch, creator of UML

How Comments Actually Get Used

Noise comments

useEffect(() => { // Check if window and window.dataLayer are available if (typeof window === "undefined" || !window.dataLayer) { } }, []);

This comment doesn’t provide any extra information. You can understand what the code does simply by reading it. If you feel that the condition expression is too complex, you can extract it into a function with a name that describes what the condition checks.

useEffect(() => { if (!isDataLayerAvailable()) { } }, []); function isDataLayerAvailable() { return typeof window === "undefined" || !window.dataLayer }

Misinformation comments

interface User { id: string username: string } /** * Fetch the user information by their username */ export function getUserById(id: string): User { await fetch(...) }

This comment is an example of a harmful comment. It directly conflicts with the function signature. What is a developer supposed to believe in this case? They have no choice but to read the code.

TODO comments

These comments are nothing but wishes and dreams. A TODO comment is a marker of “I would have done this if I had infinite time and energy.” This is not something worth recording in your source code. You will never go back and update your TODO comments. You will never see a TODO comment and think, “Huh, well, I’ll go ahead and finish this work.” We now live in the age of project managers, ticketing systems, and continuous integration. Work should be planned ahead of time in a transparent place for all team members to see, not in the source code where only developers might occasionally see it. Don’t write TODO, write a ticket instead.

Additionally, code should always be in a complete, working state every time it is merged into main. A TODO comment conflicts with the concept of continuous integration. A TODO comment is a signifier that something is incomplete, not polished, or usage could lead to crashes and errors. These are all undesirable things that we should avoid in our projects at all times. If, for a good reason, you need to commit code without finishing all parts of it, instead consider using a stub. Make it clear via naming that the stub doesn’t do anything and will need to be replaced later, and ensure that the app will not crash if used.

Commented out code

The most harmful of all comments are code that has been commented out. Whenever you see this, you should delete it immediately. Past revisions of working code can be found using version control. Learn to use your tools! That old code can do nothing except deceive; and will most likely break your application if it is uncommented. It no longer has type checking or linting working on it, and will likely reference variables and call functions that no longer exist. If code can be commented out, you won’t need it.

Doc comments

/** * @param {string} id The ID of the user * @returns {User} The user matching the ID */ export function getUserById(id: string): User { await fetch(...) }

These comments are almost always noise. The exception is the example above, when you’re exposing a public API through a package. In those cases, including low-level documentation along with the source code could be very useful to your users. Within your own apps though, you should always strive to keep your functions clean and readable using the signature alone. In this case, this comment block provides no additional insight. If there were details you needed to add, then your function is likely begging for a refactor. A comment is a last resort.

I have worked with clients where the CTO mandated that every JavaScript function had to have documentation (comments) along with it. It’s a policy that, from the jump, enforces the idea that “my developers cannot write clean code, they must provide additional English text to explain what their code does.” Not only are you insulting your developers, but you’re also mandating that they stay bad, by spending their time patching over their deficiencies by writing comments rather than writing better code. It’s a surefire way to make all your best developers leave.

Whinging

/** * Putting this here because our backend is so slow and * whenever I tell anyone it’s just ignored. */ setTimeout(() => {}, 5000) /** * The docs also say this property should be a number, but it sometimes * comes back as a string. I've told the PM this many times but it's * been broken for years */ const fixedOrderId = Number(orderId)

You won’t believe how common this is.

What You Really Want Are Unit Tests

While code can document itself, providing additional low-level documentation is a noble goal. However, comments are the actually worst way to do that. The best way to provide low-level documentation is unit tests.

Developers all crave examples. We want to see working code to avoid mistakes in our own code. There is no better way to provide examples of working code than to add unit tests. Which you should be doing anyway! Tested code allows refactoring, and refactoring allows for clean code. Clean code is always more desirable than commented code.

Summary

After reading this, I’m sure you think that I hate all code comments and think that they are always bad. I honestly don’t care too much. I still approve PRs with comments in them, and I don’t lose my mind when my pairing partner slaps a comment on something. What’s important is to break the myth that comments are always helpful. Comments are nuanced just like any aspect of programming, and can have harmful consequences just as they could have helpful ones. I hope that you will think twice before believing that a comment will fix your code, and instead seek to refactor and test.

Related Posts

Automated Dependency Management: Why Leading Engineering Organizations Are Embracing It

November 22, 2024
Highly efficient engineering organizations are automating software dependency management processes to save time and money, and most importantly reducing risks from vulnerabilities and supply chain attacks. External application dependencies play a critical role in today’s software ecosystem. By automating these processes, teams are free to focus on higher value tasks, while maintaining a secure and resilient codebase. ##

When and Why to Use Micro Frontend Architecture

November 12, 2024
As businesses grow and their technical stacks evolve, micro frontends have emerged as a practical architectural strategy for managing complexity and improving scalability. Deciding when and why to adopt micro frontends isn't just an engineering conversation; it’s one that should involve product and business stakeholders as well. Is it the right choice for your organization? What benefits, risks, and tradeoffs should you weigh before committing?

Serverless Event-Driven APIs with AWS Kinesis

October 24, 2024
Everything that happens in an application or a software system is triggered by something. Whether it’s a user action, a sensor output, a periodic trigger, an event loop, an API call, or something else entirely — our software is governed by events. Sometimes those events are implicit, like a server that handles an HTTP request and updates a database row without ever explicitly defining it as an “entity updated” event or recording the details.