Comments & Documentation
The default is no comment. Good naming, small functions, and clear types do most of the work documentation pretends to do. I write a comment only when the why is non-obvious — and I’ll delete a stale comment faster than I’ll delete dead code.
The why-not-what rule
// Bad — restates the code in English
// Increment counter by 1
counter += 1;
// Bad — describes what the function does (the name already does that)
// Fetches the user by ID and returns it
async function getUser(id: string) { ... }
// Good — explains a non-obvious constraint
// Stripe webhook IDs are not idempotent across retries when the event
// is delivered via the CLI proxy, so we dedupe on (event.id, livemode).
const key = `${event.id}:${event.livemode}`;
Comments belong where reading the code alone would leave a competent developer asking “wait, why?”
- A workaround for a specific bug (link the issue)
- A subtle invariant the type system can’t express
- A performance choice that looks weird
- A historical reason the obvious approach doesn’t work
Comments that should be deleted on sight
// TODO: refactor thiswith no date and no owner — it’ll still be there in three years// added by Ahmad for the chat flow— git blame already knows// removed login button (see PR #142)— the PR is the record, not the comment- Multi-paragraph docstrings on internal functions — JSDoc on a 4-line helper is theatre
If a TODO is real, it has a tracker link: // TODO(LIN-482): replace with real pagination once the API supports cursors.
JSDoc only on public boundaries
I don’t write JSDoc for every function. I write it for exported functions that other features (or external consumers) will call — and only when the signature alone isn’t enough:
/**
* Refreshes the access token using the stored refresh token.
* Throws ApiError(401) if the refresh token is also invalid —
* callers should treat this as "force logout".
*/
export async function refreshAccessToken(): Promise<string> { ... }
The throwing behavior isn’t visible in the type, so it earns a doc. The function name and return type already say “what” — the doc says “what could go wrong.”
Don’t document the obvious; do document the surprising
// Bad — the type says everything already
interface User {
/** The user's ID */
id: string;
/** The user's name */
name: string;
}
// Good — captures a real constraint
interface User {
id: string;
/**
* Display name. May be empty for OAuth-imported users until they
* complete profile setup. Don't render directly — use `formatUserName(user)`
* which falls back to email-local-part.
*/
name: string;
}
README and design docs
A README.md at the repo root and one in each non-trivial feature folder. The README covers:
- What is this? (one paragraph)
- How do I run it? (commands)
- How is it organized? (link to architecture notes)
- How do I add a new feature? (the path of least surprise)
For larger architectural decisions, I write an ADR (Architecture Decision Record) — short markdown file with context, decision, alternatives considered, and consequences. ADRs are append-only; if I change my mind, I write a new one that supersedes the old one. The history matters.
Comments rot. Code is the source of truth.
A wrong comment is worse than no comment. If I see one, I delete it on the spot — not because it’s a big deal, but because the next person who reads it will trust it. The same applies to my own old comments. I have no loyalty to past-me.