Skip to content
Chapter 102Lesson 2

When inline comments earn their place

The discipline of inline code comments: write only the why a reader cannot see, and promote it into compiler-enforced structure when you can.

A developer opens a function to clean it up. Partway down, on its own line, sits this:

await new Promise((r) => setTimeout(r, 50));

No comment. It looks like a leftover sleep someone forgot to delete, so they delete it. The tests pass, the PR merges. A week later a flaky race shows up in production under load, and two engineers spend half a day tracing it to that deleted line. The sleep was real work: it spaced out two calls that raced otherwise. Without a why, nobody knew not to remove it.

Same developer, different file, finds this:

// loop over the invoices
for (const invoice of invoices) {

This one should go. It says exactly what the line below says, in English instead of code, and carries nothing.

Most codebases commit both failures at once. Junior code over-comments (// increment i above i++); then a “self-documenting code” creed overcorrects and strips comments that carried irreplaceable context, like that sleep. The rule sits between them: a comment exists to answer a why the code physically cannot, and nothing else.

This is the same judgment as last chapter, one level down. There it was which declarations earn a doc block, the contract read by a caller at the call site. Here it is which lines earn an inline comment, the reasoning read from inside the file. Same instinct, that volume should track value; only the surface differs.

Run one question on any line. It has two parts, and a comment earns its place only when both answers are yes:

  1. Would a reasonable reader, looking at the code alone, ask “why is it written this way?”
  2. Is the answer invisible in the code, because it lives outside this file?

The second part does the work. A real comment carries knowledge the reader can’t reach by scrolling: a Postgres quirk, a Stripe API gotcha, a deliberate ordering, a bug the workaround prevents. The comment is the only place that knowledge can sit next to the line it constrains. If the reader can answer the question from the line itself, the comment is noise.

This is why the what never qualifies: it is the code, so a comment that restates it carries nothing from outside. // increment counter above counter++ does real harm, since noise dilutes the comments that do carry context.

Here’s the test on a pair where the line of code is identical.

// add one to the retry count
retries += 1;

The reader answered “why is it written this way?” the instant they read the line. The comment restates the code in English; nothing comes from outside the file.

The pair is near-identical on purpose. People judge comments by length or by whether a line “looks complicated,” and both tabs pass that test. Only one question separates them: does the answer live in the code, or outside it?

Knowledge from outside the file shows up as four recurring kinds of comment. The Server Action below finalizes an invoice and hits all four, one per step. A real function rarely needs all four at once; this is a teaching specimen, and its density is a signal we return to at the end.

export const finalizeInvoice = authedAction(
'member',
finalizeInvoiceSchema,
async ({ input, orgId }) => {
// Postgres truncates timestamptz to microseconds; round here first or
// the equality check in listDueInvoices misses rows by sub-µs drift.
const finalizedAt = roundToMicros(Temporal.Now.instant());
// Stripe's payment_intent.succeeded can arrive before our finalize commits;
// dedup on payment_intent_id, not arrival order.
const charge = await chargeInvoice(input.invoiceId, { idempotent: true });
// Not using listInvoiceLines() here — its join doubles the query-plan cost
// on tenant_invoices; the narrow select is deliberate.
const lines = await db.query.invoiceLines.findMany({
where: eq(invoiceLines.invoiceId, input.invoiceId),
});
// Order matters: the audit row must commit before the receipt enqueues, or
// a crash between the two loses the audit but still sends the email.
await writeAuditRow({ orgId, action: 'invoice.finalized', charge });
await enqueueReceiptEmail(input.invoiceId);
return ok({ finalizedAt, lines });
},
);

The constraint comment documents external reality the code must bend to. The constraint lives in Postgres, so roundToMicros looks arbitrary without it. Gone: someone deletes the round as redundant and due-invoice queries start dropping rows nobody can reproduce.

export const finalizeInvoice = authedAction(
'member',
finalizeInvoiceSchema,
async ({ input, orgId }) => {
// Postgres truncates timestamptz to microseconds; round here first or
// the equality check in listDueInvoices misses rows by sub-µs drift.
const finalizedAt = roundToMicros(Temporal.Now.instant());
// Stripe's payment_intent.succeeded can arrive before our finalize commits;
// dedup on payment_intent_id, not arrival order.
const charge = await chargeInvoice(input.invoiceId, { idempotent: true });
// Not using listInvoiceLines() here — its join doubles the query-plan cost
// on tenant_invoices; the narrow select is deliberate.
const lines = await db.query.invoiceLines.findMany({
where: eq(invoiceLines.invoiceId, input.invoiceId),
});
// Order matters: the audit row must commit before the receipt enqueues, or
// a crash between the two loses the audit but still sends the email.
await writeAuditRow({ orgId, action: 'invoice.finalized', charge });
await enqueueReceiptEmail(input.invoiceId);
return ok({ finalizedAt, lines });
},
);

The workaround comment names the failure mode the workaround prevents. Stripe events can arrive out of order, so the dedup keys on payment_intent_id rather than arrival time. Gone: a future reader “simplifies” the dedup to arrival order and reintroduces a double-charge under reordering.

export const finalizeInvoice = authedAction(
'member',
finalizeInvoiceSchema,
async ({ input, orgId }) => {
// Postgres truncates timestamptz to microseconds; round here first or
// the equality check in listDueInvoices misses rows by sub-µs drift.
const finalizedAt = roundToMicros(Temporal.Now.instant());
// Stripe's payment_intent.succeeded can arrive before our finalize commits;
// dedup on payment_intent_id, not arrival order.
const charge = await chargeInvoice(input.invoiceId, { idempotent: true });
// Not using listInvoiceLines() here — its join doubles the query-plan cost
// on tenant_invoices; the narrow select is deliberate.
const lines = await db.query.invoiceLines.findMany({
where: eq(invoiceLines.invoiceId, input.invoiceId),
});
// Order matters: the audit row must commit before the receipt enqueues, or
// a crash between the two loses the audit but still sends the email.
await writeAuditRow({ orgId, action: 'invoice.finalized', charge });
await enqueueReceiptEmail(input.invoiceId);
return ok({ finalizedAt, lines });
},
);

The intentional-deviation comment names the path not taken and why. A shared listInvoiceLines() helper sits right there, skipped because its join is too expensive at this scale. Gone: a reviewer or an agent “tidies” the narrow select into the helper and doubles the cost on the hottest table.

export const finalizeInvoice = authedAction(
'member',
finalizeInvoiceSchema,
async ({ input, orgId }) => {
// Postgres truncates timestamptz to microseconds; round here first or
// the equality check in listDueInvoices misses rows by sub-µs drift.
const finalizedAt = roundToMicros(Temporal.Now.instant());
// Stripe's payment_intent.succeeded can arrive before our finalize commits;
// dedup on payment_intent_id, not arrival order.
const charge = await chargeInvoice(input.invoiceId, { idempotent: true });
// Not using listInvoiceLines() here — its join doubles the query-plan cost
// on tenant_invoices; the narrow select is deliberate.
const lines = await db.query.invoiceLines.findMany({
where: eq(invoiceLines.invoiceId, input.invoiceId),
});
// Order matters: the audit row must commit before the receipt enqueues, or
// a crash between the two loses the audit but still sends the email.
await writeAuditRow({ orgId, action: 'invoice.finalized', charge });
await enqueueReceiptEmail(input.invoiceId);
return ok({ finalizedAt, lines });
},
);

The load-bearing-weirdness comment documents an ordering that is part of the contract. The audit row must commit before the email enqueues; flip them and a crash loses the audit but still sends the receipt. Gone: a refactor reorders the awaits and a rare crash ships an unaudited receipt. Hold onto this one: it’s the kind most often stripped, and the one we’ll promote into enforcement shortly.

1 / 1

Each names what breaks if it’s missing, and the reader can’t infer it from the line because the knowledge lives outside the file: Postgres’s precision, Stripe’s ordering, the cost of a join, the failure mode of a reordering. That is what makes each a why, not a what. These four aren’t invented for this lesson; they are the same inline notes a well-run codebase’s conventions allow.

Knowing a line earns a comment is half the skill; writing it is the other half. The reader has already read the code, so the comment adds only what the code can’t show, in as few words as that takes.

Three rules carry it:

  • One line, direct, declarative. Name the external fact and the action it forces.
  • No hedging, no apology, no narrating the code’s structure.
  • Use the constraint + response shape, not a description of a situation.

That third rule is where most comments go soft. Compare the same Stripe-ordering note written two ways:

// We need to handle the case where Stripe events might arrive in an
// unexpected order, so we should make sure we dedupe them somehow.
await recordEvent(event.id);

This narrates a situation and hedges instead of stating the fact and the response. “We need to handle the case where…” is throat-clearing; “somehow” admits the writer hadn’t decided. Three lines that say less than one would.

A few small conventions compound across a codebase:

Each no case comes down to the same thing: some other tool already does the job the comment is reaching for.

  • Restating the code. // loop over invoices above for (const invoice of invoices). The code says it; the comment is a second copy in a worse language.
  • Section dividers. // === HELPERS ===. If a file needs internal signposts, split the file.
  • Author and date stamps. // Created by Maria 2024-03-15. git blame knows who wrote every line and when, and never goes stale; the comment rots the first time someone edits the line.
  • Commented-out code. A block disabled “in case we need it later.” You won’t, and git log recovers it if you do. Always safe to delete.
  • Bare TODOs. // TODO: fix this, with no owner and no ticket, is a wish, and wishes pile up forever. Put the work in the issue tracker; reserve an inline // TODO(SAAS-1234): … for work shipping this sprint.
  • Fossil comments. A note explaining a workaround for a bug fixed three years ago. The workaround should be gone, and the comment with it; left behind, it tells the next reader to fear a problem that no longer exists.

The bare-TODO rule has one wrinkle you’ll meet in this course’s starter code. Stubs carry markers like // TODO(7.6.3) — implement createInvoice: not bare, but owned by the lesson that builds the stub and due when it lands. Same rule, different form.

The fossil and the dead block show that a comment can lie: the code moves on, the comment doesn’t. Catching that before it merges is a review problem the next lesson picks up.

Spotting noise is the easy half; writing the right comment under pressure is the half that matters. For each snippet, decide the verdict, and when you keep it, get the voice right.

For each snippet, pick a Verdict. When you choose Rewrite, the Replacement dropdown wakes up — pick the comment that earns its place. Then press Check.

  1. Comment 1
    // increment i
    i += 1;
  2. Comment 2
    // We need to make sure we handle the situation where the two writes
    // could happen in the wrong order and cause problems.
    await writeAuditRow(entry);
    await enqueueEmail(entry.id);
  3. Comment 3
    // JS amounts are IEEE-754 doubles; round to whole cents before compare or 0.1 + 0.2 fails.
    if (Math.round(amount) !== expected) {
  4. Comment 4
    // const taxRate = legacyRate(region); // old way, leaving for reference
    const taxRate = currentRate(region);

Where the why belongs: comment vs. TSDoc vs. ADR

Section titled “Where the why belongs: comment vs. TSDoc vs. ADR”

A why can live in three places. Earlier chapters covered two: rationale on a public surface with TSDoc, and an architectural decision in an ADR . This lesson adds the inline comment. The risk now isn’t writing a bad why; it’s writing a good one in the wrong place.

What separates the three is who reads it, from where, and at what scope. Each panel pairs a line that belongs on that surface with one that doesn’t.

Inline // why
Who reads it
Someone editing this file
From where
Inside the function, on the line
Scope
Local — one line or block
Belongs here
// audit row must commit before the email enqueue A one-line constraint, right next to the code it constrains.
Not here
// Why we chose Drizzle over Prisma Architectural scope — that's an ADR.
One line or block, read from inside the file.

Scope decides between comment and ADR: a one-line constraint (“Why await sleep(50)?”) is a comment, a decision that constrains the whole codebase (“Why Drizzle?”) is an ADR. Rationale never goes in TSDoc, since a caller hovering a name wants the contract, not your reasoning about a line.

A comment is not decoration beside the code; it is part of the code. When a function is refactored, the // why comments on its lines have to move with those lines. They are not cleanup to sweep away.

Watch how that goes wrong, tracing one load-bearing ordering comment through three releases.

lib/invoices/finalize.ts
// Order matters: the audit row must commit before the receipt enqueues,
// or a crash between the two loses the audit but still sends the email.
await writeAuditRow(entry);
await enqueueReceiptEmail(entry.id);
Before: the ordering comment guards two awaits that must run in this order.
lib/invoices/finalize.ts
// Order matters: the audit row must commit before the receipt enqueues,
// or a crash between the two loses the audit but still sends the email.
await persistInvoiceResult(entry);
The refactor: a tidy-up extracts the two writes and drops the comment as 'obvious'.
lib/invoices/persist.ts
const persistInvoiceResult = async (entry: InvoiceResult) => {
await enqueueReceiptEmail(entry.id);
await writeAuditRow(entry);
};
Three releases later: someone reorders the awaits inside the helper. No comment warns them, and the audit-loss bug is live again.

The bug returns three releases after the warning was deleted, the worst possible distance: far enough that nobody links the cleanup PR to the failure, close enough that the code still looks fine. The comment was the only thing between “two awaits whose order matters” and “two awaits in any order,” and a tidy-up erased it without asking what it was for.

So here is the reflex, run whenever a refactor would delete a comment:

  1. Stop, and understand what the comment was preventing.
  2. Then either carry the comment with the code it explains, or replace it with structural enforcement that makes the bug hard to write.

That second branch is the upgrade, and it rests on the key idea here: a comment is the cheapest and weakest form of structural enforcement. It documents that a constraint exists but does nothing to stop you from violating it. It is the fallback for when the type system can’t express the rule.

Promote the comment to enforcement when you can

Section titled “Promote the comment to enforcement when you can”

When a comment is preventing a real bug, the move that pays off isn’t writing it more carefully. It’s asking: can I move this constraint from prose into something the compiler or runtime enforces?

Three promotions cover almost every case, and each builds on something earlier in the course:

  • An ordering constraint becomes a transactional function. // audit must commit before the email enqueue becomes one function that does both inside a transaction, in order. There’s now no way to write the awaits out of sequence, so the comment dissolves. This is the thin-action, pure-lib shape you already use.
  • A “remember to validate” becomes a Zod parse. // validate the input first becomes a safeParse at the boundary, the parse-on-entry discipline from a new angle. The validation is now enforced, not remembered.
  • A “must be called after auth” becomes a typed argument. // only call this with an authenticated user becomes a function that takes an authenticated-session type, or runs behind the authedAction wrapper. Skipping auth is now a compile error, not an honor-system comment.

Here’s one end to end: the same logic leaning on a comment, then on the compiler.

// Remember to validate input before calling this.
export const createInvoice = async (input: unknown) => {
return db.insert(invoices).values(input as NewInvoice);
};

The constraint lives in prose, and nothing stops a caller from skipping it. input is unknown and gets cast straight into the insert. The comment is a hope: one forgetful call site and unvalidated data reaches the database.

Not every comment can make that jump. A Postgres-precision constraint or a Stripe out-of-order fact describes an external system, and no type can express that behavior, so those stay comments, well-written ones. Telling which constraints can become code and which must stay prose is the skill.

Which of these comments should be promoted out of prose and into something the compiler or runtime enforces? Pick every one that can be — leave the rest as well-written comments.

// callers must run safeParse on the body before this touches the DB
// don't reorder: the audit insert has to land before we enqueue the email
// never reach this path unless the request already passed the auth check
// timestamps come back rounded to the microsecond, so compare rounded values
// the same webhook can land twice and in either order

If a function needs three or more // why comments to be understood, the function is probably the problem: doing too much, or sitting on the wrong abstraction. One comment flags something non-obvious; a cluster of them flags a design with room to improve. When you reach for the third, treat it as a prompt to split the function or shift the abstraction.

The finalizeInvoice specimen carried all four kinds in one function, useful for showing the shapes side by side, but in real code that density is exactly the smell: it probably wants to be two or three smaller functions, each with one comment or none.

So a comment earns its line when it passes two checks: before you write it, the why must live outside the code; after, the constraint must be one the compiler can’t carry for you.

Two essays take this principle further than a rule can.