On comments

For the past decade or so, my syntax highlighting configuration has had an odd quirk in it: code comments are bright orange, not the standard faded grey. This wasn't an accident. I wanted to force myself to get better at documenting code, and what better way to do so than to make comments extremely in-your-face so that any extraneous noise becomes painfully visible.

It's very easy to write useless comments in code; comments that convey little beyond what can already be surmised from the nearby code, that state obvious facts, or that only make sense to the person who wrote them at the time that they wrote them. Such comments stick out like a sore thumb when reading through code (especially with my configuration). They read like noise, polluting the flow of the code with irrelevant information that causes the brain to lose its thread in the code. This is especially apparent when comments reflect some previous iteration of the code, but is no longer accurate, and thus causes the reader to do a double-take and start doubting their own understanding.

And yet, comments can be hugely valuable when used well. When a comment points out why the "obvious" restructuring of a bit of code won't work, explains why a particular configuration option needs to be set in a library, or highlights a corner case that the code doesn't support (and why), it can save the reader hours or days of work and frustration. And remember, that reader could be yourself 12 months from now when you've forgotten all the details about how the code in question works!

Often, you won't even realize how much time and effort a single comment might have saved you, as you just read it and move on. It only becomes apparent if you consider what you would have done if you had read the code without the comment, spotted something weird-looking, and decided to see if you could make it better.

I'm specifically talking about code comments, not in-code documentation (eg, /// in Rust). Documentation is written for your consumers. Comments are written for your collaborators. And while some of these lessons apply for the former, I only want to talk about the latter (for today at least).

Just write more readable code!

It's no wonder that many engineers have a complicated relationship with comments. We love them when they're good, hate them when they're noise, and (perhaps subconsciously) worry about writing too many of them in case they're just noise to others. My experience has been that some engineers write fewer and fewer comments as they become more senior, preferring instead to make their code easier to read by breaking up code, using more descriptive function and variable names, leveraging the type system more extensively, etc.

Those techniques are all worthwhile and good practice, and they do result in code that's easier to read and understand. But they do not replace comments. Not fully. Improving the readability of the code will, in general, make the "how" clearer, but not the "why". Some limited comment-like code constructs exist, such as Option::expect and unreachable! in Rust, where you can pass in a string explaining why something is definitely Some or unreachable!, but they primarily document invariant assumptions and not much else. Information that was never written down cannot be recovered by reading harder.

What kinds of comments are useful?

There are a number of types of comments that in my experience are nearly always worthwhile.

The main ones are:

TODOs

You know the code isn't complete, finished, or fully polished, but you also know it's not important that it isn't. So, you leave a note to a future developer about what is outstanding and ship the thing. Risky, perhaps, but the reality of software is that there are a lot of corner cases, and we can often know that some simply aren't important at the moment (ranging from "this could be optimized" to "this corner case is complicated to implement but also highly unlikely to be relevant").

The main thing to remember with TODOs is to actually leave enough information for the reader to understand what you meant. When everything is fresh in our minds, it's easy to just jot down a few keywords, but when the junior engineer is looking at it in six months time and you've left the team, that TODO is probably going to remain just that — a to-do. Multi-line TODOs are totally fine, as are outlines / partial steps for how to resolve, and chances are the person who comes after you (or you yourself!) will be happy you took the time.

Examples:

Linking a TODO to a ticket can also be a good idea to have a space for discussion and tracking while still keeping the strong connection to the code.

I'm personally not too fussed about the exact format of TODOs. They need to contain the letters TODO (in that order), but whether you add a name in there and whether it's always a prefix isn't terribly important. At Helsing, we require that a TODO always includes the author's name, though if you don't go that way you can always recover the name via git blame.

References

Code that has a strong connection to some external source, such as if it's a (lightly modified) copy of some code that lives elsewhere or if it's an encoding of an algorithm specified in a paper, blog post, or book, usually benefits from that connection being highlighted in a comment. For code and blog posts, this often means a link (specifically, a permalink — press y on GitHub before copying the link), whereas for published resources it may mean title + author + page/chapter number.

In addition to the reference pointer, any divergence from the reference should also be documented (and motivated).

Examples:

Correctness arguments

Proofs (informal or formal) about why some non-trivial code ends up doing the right thing usually warrant a comment. The code itself represents the steps taken (the how), and tests can verify that the outcome is right (the what), but the reasoning for why those steps reliably lead to that outcome should be written down to enable improvements to the what in the future.

Correctness arguments often pair well with assertions (eg, unreachable!) where possible, in particular to validate (and highlight) assumptions of invariants that the code makes.

Examples:

Pro-tip: don't stop in the middle of a correctness argument when you realise it doesn't hold and then commit the partial proof. I did that.

Hard-learned lessons

Any time you've spent more than 30 minutes getting something to work, and the fix ended up being some brief but unintuitive magical incantation that just has to be right there, that warrants a comment. The reasoning is pretty straightforward: you yourself did not realize 30(+) minutes ago that it was needed, so it will almost certainly not be intuitive to someone else new to the code why it's needed either! And even if you don't actually know why the incantation is needed, document how you arrived at it, and what happens if it is absent, so that those who come after you can continue from where you left off.

Examples:

Rationale for constants

We've all read (and, let's be honest, written) code like this:

const HEARTBEAT_INTERVAL: usize = 5;
max_packet_size = 1492
range = (i > 0 && i < length - 1 ? 26 : 27)
type Identifier = u16

But do you remember why the max packet size was 1492? Was it required for correctness? What about the heartbeat interval — was 5 (seconds presumably) chosen randomly or through thorough testing? Why was 16 bits deemed enough for Identifier, and why was it necessary to limit the number of bits in the first place?

For constants like these (often called "magic numbers"), a comment explaining what the constant represents (if it doesn't have a name already), how it was chosen, and the consequences of changing it, is usually warranted. Sometimes, merely giving the constant a name is sufficient, whereas other times several paragraphs about testing methodology may be worthwhile. Use your judgement!

And remember, it's fine to say a value was chosen mostly at random — that's still useful information to the reader who considers changing it!

Examples:

Load-bearing choices

If the correctness of a piece of code depends on a seemingly-innocuous implementation detail elsewhere, that implementation detail better have a comment highlighting this fact. Whether it's

we must collect into a BTreeSet here as the code below assumes that iteration is ordered

or

this type must never be constructable outside of this module to ensure no undefined behaviour,

these kinds of load-bearing code invariants are vital to write down. Without them, an innocent developer may come along much later and swap the BTreeSet for a HashSet to improve performance or export a type for convenience, not realizing that they just broke prod.

Hopefully it goes without saying that you should avoid writing code with such invisible invariants if at all possible (eg, using types in languages with expressive type systems). And that having good assertions and testing is important to catch things like this. After all, sometimes people don't read comments even if they're perfectly placed and phrased. But sometimes the best we can do is a strategically placed comment.

Examples:

Algorithm outlines

Code is, inherently, the encoding of algorithms in computer-understandable language. Sure, those algorithms vary wildly in complexity, but really it's all just sequences of steps. Often, the algorithm is clear enough from the implementing code that outlining each step would just be noise. But as the implementing code grows, the (perhaps simple) algorithm can sometimes get lost among the syntax. In cases like this, it's useful to highlight the high-level, relatively easy to understand abstract steps that the code is implementing. Either up-top as an outline of what's to come, or interspersed with the code so readers can identify which "part" they're reading. In a sense, this is a lightweight form of literate programming.

Examples:

"Why not"s

When a segment of code, on purpose and for good reason, violates standard conventions, eschews widely-accessible types and helper functions, or is otherwise peculiar or idiosyncratic, having that reasoning written down for posterity is important. Otherwise, others will be forced to re-learn it. In many ways, this is a corollary to "hard-learned lessons": rather than "why are these lines of code necessary?", this is "why didn't you do it this (obvious) other way?".

Examples:

Intentional trade-offs

When building most software, there are times when you have to make a trade-off. You evaluate a few different options, decide one has the highest net-positive profile (or doesn't have unacceptable negatives), and then move forward with that decision. That decision should be documented. Otherwise, inevitably, down the line, someone will question why X was chosen over Y. If you're still around, and still remember, all may be well, but if that isn't the case it can lead to re-threading the same discussion every few years. Added up across all such choices at the company, that's a significant time drain!

Now, using comments to address this is a bit tricky, since the decision may not live in one place in the code. Architectural Decision Records (ADRs) are one way to document them, but they live separate from the code (though usually within the same repo), which makes them easy to forget to look for, and easy to let go stale.

My most recent attempt at squaring that circle is to write ADRs in a much more concise form directly in comments placed next to the code they justify. The format I landed on is called Y-Statements, and they are of the form:

In the context of <use case/user story u>, facing we decided for

The format is strict to ensure that all those parts are considered every time; you can't skip any of the fields, and you are also supposed to write concise sentence fragments into each placeholder. No substituting any of these with a paragraph of text (though you can add paragraphs afterwards to add further context).

By having these ADRs right by the code, the decision logic for why that code does what it does is easily discoverable, and it's easy to notice that it needs updating when the code is updated. The cost is that the decisions become invisible from the outside; you can no longer survey what a project has decided without reading all of it.

So, at Helsing we've written a small tool to lint such comments, and to easily extract them from the code. We're open-sourcing it today as yadr.

yadr walks a source tree and lists every decision it finds:

$ yadr ls src/
==> src/crdts.rs
 -> 2024-05-06 Removal of `.alive` tracking
==> src/causal_context.rs
 -> 2024-04-19 Allocation of identifier bits

You can also use yadr show 2024-05-06 to print any one of them in full, and yadr check to assert (eg, in CI) that all such Y-Statements follow the convention. That last one is deliberately strict about punctuation and phrasing,

Given this is a new tool, there aren't many public examples of them yet, although you can see some in the dson CRDT crate we've published:

Comments are technical writing

Comments are technical writing, and should be written with the same care as other technical documents. This is not the place to go take a deep-dive into technical writing, but there are three key aspects of good technical writing that are worth highlighting about comments in particular:

Bytes are cheap. The byte weight of a source code file is mostly irrelevant today, and especially so the number of bytes spent on comments. There is no reason to be overly terse, especially if it may hinder your tired, undercaffeinated, and in-a-rush self from understanding what you meant a year or two down the line. Use more words if it is helpful, make use of punctuation and full sentences, and avoid all but the most obvious of abbreviations and acronyms.

Remember the reader. It's easy to make the mistake of assuming that future readers will have all the same context, insight, and recently-acquired clarity as you have when writing. Words like "of course", "trivial", "obviously", and "just" are good indicators of this attitude. Try reading your comment while suppressing your knowledge of the code (and its invariants) and see if it still makes sense.

Precision matters. As in most technical writing, the details can be hugely important. Inaccurate or ambiguous use of punctuation, poor grammar, typos and missing words, and invalid references to variables or functions can at worst lead the reader to the wrong conclusions, and at best cause friction when reading. Read over your comments an extra time to make sure it actually says what you meant.

What changed with LLMs?

With the adoption of LLMs, a lot of code is now written (and read) by agents. But this does not reduce the importance of carefully considered comments; quite to the contrary!

Coding agents arrive at your file without any knowledge of why that retry loop is there, what happened in the design discussion on Teams, and no way to tell which choices embedded in the code were deliberate and which were arbitrary. It's like a new engineer to the team who is fast and confident (and has endless energy), but knows nothing about why your codebase is the way it is. They are precisely the target audience for comments that convey information beyond what the code itself can tell you (ref the earlier argument about the self-documenting code fallacy).

Comments also have the benefit of being directly injected into the agent's context window. The agent doesn't need to realize there's an ADR somewhere else, or happen upon that particular historical MR description, commit message, or Jira ticket description.

I've also found that LLMs are pretty good at writing these kinds of comments (and, relatedly, git commit messages), as long as you explicitly instruct them to use the insights into the "why" gained from the currently active session. This is because over the course of building something with LLM assistance, what you are providing to the model (and what the model is discovering and telling you) is exactly that additional context that should accompany the code.

LLMs are also great for sanity checking that comments still match their associated code, and to double-check that the reasoning those comments present still holds up to the realities of the broader software.

A useful heuristic is to look for corrections you repeatedly have to provide to agents, and embed those as strategically placed comments, rather than as additional stanzas in your AGENTS.md.

Closing thoughts

I hope that I have convinced you not only that useful code comments exist, but also that some (valuable) information can only be conveyed via comments. And that spending time on improving the writing of those high-value comments is often warranted.

However, I also want to leave a word of caution about mandating comment perfection. As developers, we have a limited number of spoons to allocate to the tasks we take on (and the ensuing discussion and revisions in code review), and we can easily run out of them iterating on comments (or similar bikeshed-shaped task). Sometimes, it's worth landing good code with sub-optimal comments, and treating the improvement of those comments as a separate task. This also comes with the advantage that the comment task can be undertaken by someone other than the code author, and who can thus more easily write code free of the author's implicit knowledge.

Remember: there is no "correct" ratio of comments to code, nor is it reasonable to require that "all" code be commented. Good commenting means identifying and filling information gaps in the code to aid future readers, which requires judgement, empathy, and foresight — it is not a mechanical task. If it starts to feels like one, reconsider whether the comment you're writing is truly warranted.