How To Write Good Comments

July 06, 2020 9 minute read

Comments - a powerful feature of each programming language that we either abuse or avoid as the plague. Undoubtedly one of the everlasting controversial topics of software engineering. How to write good comments? Are they needed at all? Should code be just code?

I believe that programming languages can’t be completely self-documenting. Each one of them has its constraints. It has a vocabulary of words that we can use to describe the logical flow of our operations. We can introduce new types and structures but there is contextual information that can’t be transferred through code.

As engineers we need to express ourselves in the means of the programming language we’re writing in. Comments are a way to break out of its limitations. They let us introduce concepts and reasoning from the analog world that have no place in the executable source code.

The code allows us to describe the operations that we want to achieve. To guide the computer through the actions that our software needs to do. Through comments we can use the richness of a spoken language (most often English) and tell the reader the “why”. It removes all boundaries and allows us to write a few excessive characters that are of under no obligation to the compiler.

But with such a powerful feature comes the responsibility to use it well. The proper way to handle comments is and will be an ongoing debate for as long as this industry holds. Comments are a powerful way to provide context but used incorrectly, they lead to confusion and obscurity.

There are those that see comments as something that should be eradicated from every codebase. A sign that the code is not written well enough. Something that should be refactored and removed as soon as possible. Maybe I’m not such a purist but I don’t share that opinion. To me, well-written comments make the codebase even better.

In this article I’ll share a few different points about the proper use of comments. When they are great and when they can be good, ugly or even obnoxious.

Don’t Compensate with Comments

Before we go into the practicalities, we need to get something out of the way. Comments shouldn’t be compensating for badly written code. If you can express your whole intention through code, then you should do so.

Make sure that someone who doesn’t have your familiarity with the codebase can understand “what” you’re doing. Use descriptive names and structure your code well.

Once you’ve done that, if you still think that some extra information would be useful to the reader - write a comment. If you put one to make your code more understandable you’re doing double the work.

Comments Should Tell The “Why”

One of the most common opinions you’d hear is that if the code is well-written it is self-documenting. If you need to write a comment, your code is not written well enough.

Yes, the only sure way to understand how a program works is to go through its code. It is the single source of truth. But the logic we write often represents ideas that come from the outside world. The calculations we make are based on the laws of physics. The way we manage data is based on the analyst’s needs.

Not all information can be communicated through the code alone. Naming and formatting allow us to pass some of this information to the reader but they don’t provide the reasoning. On its own, code falls short to describe “why” we have written the logic in a specific way.

There are subtle but important operations whose order matters. Small obscure details dictated by the needs of the business we are building the product for. Comments are a great way to pass context for such scenarios. You wouldn’t name a variable binaryTreeBecauseLinkedListSlower but you’d absolutely provide the reasoning behind your choice of data structure in a comments above it.

Code can be self-documenting but only to a certain point. Making it more descriptive than that would make reading slower and confusing. Through comments we can tell the “why” and leave the code to express the “how”.

Comments Define Abstractions

An abstraction is a single idea or entity that hides the details of something more complicated underneath. In that sense, code is not a good enough tool to describe abstractions. Code is detailed, but abstractions are vague.

We shouldn’t have to read through the source to understand what a module does. An abstraction is useful exactly because we don’t have to know the implementation. Comments can provide details and context that can be derived from the interface. They let us give a simpler high level vew of a module or a class that is neither too abstract, nor too specific.

The interface shows you the basic operations, the implementation gives you the finer details. Comments sit in the middle - they describe how an abstraction should be used, its limitations and capabilities.

The Trouble With Inline Comments

We can split comments in two major categories. Those that describe a whole module, class or function. And those that sit in between the lines of the actual implementation. The first are more high level and describe what the module or function does. The latter, also known as inline or implementation comments, provide additional information about each operation.

In reality, when developers complain about the usefulness of comments they most likely mean inline comments. I like the idea in this article that they should be seen as an invitation for refactoring. An inline comment could be used to specify the logical parts of a function. This is a sign that the function could be split further instead.

// This function is long so we use comments to specify its parts.
function createUserAccount(email, password) {
    // Validate if the email is vacant.
    ...
    // Create the new user entity.
    ...
    // Send an account confirmation email.
    ...
}

// Instead we could split it in multiple smaller ones.
function createUserAccount(email, password) {
    validateEmail(email)
    createUserEntity(email, password)
    sendConfirmationEmail(email)
}

Comments are also used to describe an edge case or other interesting scenarios that need exceptional handling. Instead of inlining that logic with a comment we could again extract it and name it properly.

// Doing those checks inline becomes verbose
if (typeof IntersectionObserver === undefined) {
  // We need to polyfill the IntersectionObserver in Safari
  await import('intersection-observer')
}

// Instead we can move the check inside the function and provide a function level comment.
// Anyone using it will know that the edge case is handled by the function itself

/**
 * Return an instance of the IntersectionObserver API.
 * If it's not supported in the current browser, the function imports a polyfill instead.
 */
async function initIntersectionObserver() {
  ...
  if (typeof IntersectionObserver === undefined) {
    await import('intersection-observer')
  }
  ...
}

Inline Comments Should be Precise

Inline comments still have their uses - they provide low level details. Since they are closer to the implementation they can be more precise. We can use them to provide business details. Context from the domain that this software is built for. Details about a particular term and what it represents.

// Each Dungeons & Dragons character is proficient in some skills and gets a bonus for them.
const modifier = baseModifier + (isProficient ? 2 : 0)

Most functions don’t need inline comments. If they are short and well named then additional comments could be verbose. But longer functions or more complicated ones could use the clarity provided by the comments. As long as they keep to describing “what” and “why” we are doing and leave the “how” to the actual implementation.

Knowing Where to Add Clarity

We mentioned that obvious comments should be avoided. They lead to extra writing and extra maintenance but provide no additional benefit. We must be careful where we try to add clarity and where we refrain from that.

A developer who’s been working on a problem for quite some time may not consider a detail worthy of an additional comment. But for someone that’s new to the domain or the project it could make the difference between understanding and confusion.

Once we wrap our heads around some logic we see it in a more simple manner. So we can overlook the more complicated parts and skip documenting them. An edge case may be obvious to us but not to the other team members who are not familiar with the module we’re working on.

To make sure that complex logic is well explained we should rely on code reviews. As others are examining our code we need to listen to their questions. If we find that more than one person is confused by something it means that it needs clarification no matter how simple it seems to the writer.

The Case of the Deep Copy

Some time ago I was going through a module that a colleague of mine had written. I came across an interesting line that immediately grabbed my attention.

const data = JSON.parse(JSON.stringify(response))

When I find a weird line like this I’m immediately convinced that it has some purpose behind it. But I had no way to understand why it was done this way. There was no comment to tell me.

I grabbed my colleague and he gave me the details. The response was a deeply nested structure which was used in different places. To avoid mutating it by reference we needed a deep copy. This was the easiest way to do it.

Now that I have the context it became obvious to me. How did I not think of it before? If I find the same line somewhere else I’ll know why he did it. But until he told me I was absolutely puzzled.

The way he’d left this line would force anyone who doesn’t understand it to stop by his desk and ask him. Instead of taking 20 minutes each time someone needs an explanation he could’ve done it beforehand by adding a simple comment. A line of text in this case would’ve been equal to telepathy.

// make a deep copy to to avoid changes by reference
const data = JSON.decode(JSON.encode(response))

As we mentioned earlier, each inline comment could be an invitation for refactoring. This one provides much needed details. But they aren’t that important to be read by every developer going through the module. We could extract it into its own function that would remove the need of an inline comment altogether. Anyone that wants to learn more could read the function’s one.

You could argue that a description of a properly named function is unneeded but in this case I’d rather keep it. It’s a chance to let someone continue with their work instead of googling what a deep copy is. Don’t shy away from being verbose when you’re sure it could bring clarity.

/**
 * Returns a copy of a deeply nested data structure.
 * This is used to avoid modifications by reference.
 */
function makeDeepCopy(response) {
  return JSON.decode(JSON.encode(response))
}

const data = makeDeepCopy(response)

Imagine someone seeing the code without comments and deciding to make a quick performance improvement by removing the parse and stringify calls. Nothing would break immediately but it will lead to interesting bug tracking down the line.

Imagine that the author left the company a few months later and there was no one to shed light on the deep copy problem. Comments could act like time travel. At any given time you can read through them and find out what the person had in mind when he was writing that algorithm.

Maintaining Comments

Something that developers underestimate is that comments need to be maintained too. Code rarely stays the same for long. Few are the modules that get the benefit of being frozen in time. Shifting requirements or a change in technologies may require us to rewrite ot refactor a portion of the code at any time.

But it’s not just the code that will need taking care of - the comments as well. If you have to change a function in a weird way to handle a bug, you should document it with comments. If you need to handle a rare business case you should document why you’re doing it. If you change existing code, the comments that document it become obsolete and you need to update them.

Comments that are out of sync are worse than missing ones. If there’s no documentation for a complicated algorithm you’ll likely look for help. But if the comments are incorrect they will mislead you and that will cause more harm.

Keep Commenting

Comments are a tool and the way you wield it will decide your outcomes. If you go overboard with them you will be taxed with maintaining them and keeping them up to date. If you don’t use them at all you better write code like Hemingway or the mental model that you need to keep in your head will grow with each feature.

Comments have their purpose - to tell everything that you can’t describe with code. To break out of the limitations of the programming language. They could describe the “why” behind an operation, the reason you picked a data structure or the smaller details of an algorithm. Use them with care for every character in the file is one that you need to maintain afterwards.

Tao of Node

Learn how to build better Node.js applications. A collection of best practices about architecture, tooling, performance and testing.