Some programmers never comment their code, while others comment too much. The former believe code should be self-documenting, while the latter have read somewhere that they should comment every line of their code.
Both are wrong.
I don’t believe in self-documenting code. Yes, we should rewrite unclear code to make it more obvious and use meaningful, correct names, but some things can’t be expressed by the code alone.
Commenting too much doesn’t help either: comments start to repeat the code, and instead of aiding understanding, they clutter the code and distract the reader.
Some programmers have a habit of creating a new function whenever they need to explain a block of code. Instead of writing a comment, they turn the comment text into a function name. Most of the time there’s no benefit, and often it reduces code readability and clarity of comments (function names are less expressive than comment text).
Here’s a typical example of code I usually write:
Let’s try to replace comments with function calls:
The main condition (is directory?) is now more visible and the code has less nesting. However, the openExistingFile() method adds unnecessary indirection: it doesn’t contain any complexity or nesting worth hiding away, but now we need to check the source to see what it actually does. It’s hard to choose a name that is both concise and clearer than the code itself.
Each branch of the main condition has only one level of nesting, and the overall structure is mostly linear, so it doesn’t make sense to split them further than extracting each branch into its own method. Additionally, comments provided a high-level overview and the necessary context, allowing us to skip blocks we aren’t interested in.
On the other hand, the isDirectory() and ensureFile() are good examples of methods, as they abstract away generic low-level functionality.
Overall, I don’t think that splitting a function into many small functions just because it’s “long” makes the code more readable. Often, it has the opposite effect because it hides important details inside other functions, making it harder to modify the code.
Another common use for comments is complex conditions:
In the code above, we have a complex condition with multiple clauses. The problem with this code is that it’s hard to see the high-level structure of the condition. Is it something && something else? Or is it something || something else? It’s hard to see what code belongs to the condition itself and what belongs to individual clauses.
We can extract each clause into a separate variable or function and use comments as their names:
In the code above, we separated levels of abstraction, so the implementation details of each clause don’t distract us from the high-level condition. Now, the structure of the condition is clear.
However, I wouldn’t go further and extract each clause into its own function unless they are reused.
Info We talk more about conditions in the Avoid conditions chapter.
Good comments explain why code is written in a certain, sometimes mysterious, way:
If the code is fixing a bug or is a workaround for a bug in a third-party library, a ticket number or a link to the issue will be useful.
If there’s an obvious, simpler alternative solution, a comment should explain why this solution doesn’t work in this case.
If different platforms behave differently and the code accounts for this, it should be mentioned in a comment.
If the code has known limitations, mentioning them (possibly using todo comments, see below) will help developers working with this code.
Such comments save us from accidental “refactoring” that makes the code easier but removes some necessary functionality or breaks it for some users.
High-level comments explaining how the code works are useful too. If the code implements an algorithm, explained somewhere else, a link to that place would be useful. However, if a piece of code is too difficult to explain and requires a long, convoluted comment, we should probably rewrite it instead.
I like todo comments, and I add plenty of them when writing code. Todo comments can serve several purposes:
Temporary comments that we add while writing code so we don’t forget what we want to do.
Planned improvements must haves that weren’t yet implemented.
Known limitations and dreams nice to haves that may never be implemented.
Temporary comments help us focus on the essentials when we write code by writing down everything we want to do or try later. Such comments are an essential part of my coding process, and I remove most of them before submitting my code for review.
Info You may encounter various styles of todo comments: TODO, FIXME, UNDONE, @todo, @fixme, and so on. I prefer TODO.
Comments for planned improvements are useful when we know that we need to do something:
It’s a good idea to include a ticket number in such comments, like in the example above.
There might be another condition, like a dependency upgrade, required to complete the task:
This is a hell of a comment!
Comments for known limitations and dreams express a desire for the code to do more than it does. For example, error handling, special cases, support for more platforms or browsers, minor features, and so on. Such todos don’t have any deadlines or even the expectation that they will ever be resolved:
Tip Maybe we should start using DREAM comments for such cases…
However, there’s a type of todo comments I don’t recommend — comments with an expiration date:
The unicorn/expiring-todo-comments linter rule fails the build after the date mentioned in the comment. This is unhelpful because it usually happens when we work on an unrelated part of the code, forcing us to deal with the comment right away, most likely by adding another month to the date.
There are other conditions in the unicorn/expiring-todo-comments rule that might be more useful, such as the dependency version:
This is a better use case because it will fail only when someone updates React, and fixing such todos should probably be part of the upgrade.
Tip I made a Visual Studio Code extension to highlight todo and hack comments: Todo Tomorrow.
I like to add examples of input and output in function comments:
Such comments help to immediately see what the function does without reading the code.
Here’s another example:
In the code above, we don’t just give an example of the input and output, but also explain the difference with the original rehype-slug package and why a custom implementation exists in the codebase.
Usage examples are another thing to include in function comments:
Such comments help to understand how to use a function or a component, highlight the necessary context, and the correct way to pass parameters.
Tip When we use the JSDoc @example tag, Visual Studio Code shows a syntax-highlighted example when we hover on the function name anywhere in the code.
We’ve talked about useful comments. However, there are many other kinds of comments that we should avoid.
Probably the worst kind of comments are those explaining how code works. They either repeat the code in more verbose language or explain language features:
Or:
Such comments are good for coding tutorials, but not for production code. Code comments aren’t the best place to teach teammates how to use certain language features. Code reviews, pair programming sessions, and team documentation are more suitable and efficient.
Next, there are fake comments: they pretend to explain some decision, but they don’t explain anything, and they often blame someone else for poor code and tech debt:
Why do Chinese and Koreans need a different time format? Who knows; the comment only tells us what’s already clear from the code but doesn’t explain why.
I see lots of these comments in one-off design “adjustments.” For example, a comment might say that there was a design requirement to use a non-standard color, but it won’t explain why it was required and why none of the standard colors worked in that case:
And by lots, I mean really plenty:
Requirement is a very tricky and dangerous word. Often, what’s treated as a requirement is just a lack of education and collaboration between developers, designers, and project managers. If we don’t know why something is required, we should always ask. The answer can be surprising!
There may be no requirement at all, and we can use a standard color from the project theme:
Or there may be a real reason to use a non-standard color, that we can put into a comment:
In any case, it’s our responsibility to ask why as many times as necessary; otherwise we’ll end up with mountains of tech debt that don’t solve any real problems.
Comments enrich code with information that cannot be expressed by the code alone. They help us understand why the code is written in a certain way, especially when it’s not obvious. They help us avoid disastrous “refactorings”, when we simplify the code by removing its essential parts.
However, if it’s too hard to explain a certain piece of code in a comment, perhaps we should rewrite such code instead of trying to explain it.
Finding a balance between commenting too much and too little is essential and comes with experience.
Start thinking about:
Removing comments that don’t add anything to what’s already in the code.
Adding hack comments to document hacks in the code.
Adding todo comments to document planned improvements and dreams.
Adding examples of input/output, or usage to function comments.
Asking why a commented requirement or decision exists in the first place.