Documentation & Comments
When and how to write comments, and the difference between explaining why vs what.
Overview
Code documentation exists at three levels: inline comments explaining why (not what), doc comments generating API references (JSDoc, YARD, RDoc), and higher-level architecture documentation. The primary principle is that code should explain what it does; comments explain why decisions were made, especially non-obvious ones. Outdated comments are worse than no comments.
Origin
Literate programming (Donald Knuth, 1984) proposed interleaving documentation and code as primary artifacts. JavaDoc (Sun Microsystems, 1995) standardised machine-readable doc comments. JSDoc adapted the syntax for JavaScript. YARD (Loren Segal, 2007) added type annotations to Ruby's RDoc syntax. The "Documentation as Code" movement (Spotify, Google) treats docs with the same review rigour as source code.
Examples
TSDoc comments for a public API function
/**
* Calculates the final price for an order after applying a discount code
* and the applicable tax rate for the customer's region.
*
* Tax is computed on the post-discount subtotal to comply with EU VAT
* rules (Directive 2006/112/EC). Do not apply tax before discount.
*
* @param subtotal - Pre-discount order total in the smallest currency unit (cents)
* @param discountCode - Optional promotional code. Pass null if no discount applies.
* @param taxRate - Decimal rate (0.2 for 20% VAT). Must be between 0 and 1.
* @returns Final price in cents, rounded down to avoid overcharging.
*
* @throws {RangeError} If subtotal is negative or taxRate is outside [0, 1].
* @throws {InvalidDiscountError} If discountCode is non-null but not found in the database.
*
* @example
* // 100 EUR order with 10% discount and 20% VAT
* const total = calculateFinalPrice(10000, 'SAVE10', 0.2);
* // => 10800 (90 EUR + 18 EUR VAT = 108 EUR)
*/
export function calculateFinalPrice(
subtotal: number,
discountCode: string | null,
taxRate: number
): number {
if (subtotal < 0) throw new RangeError('subtotal must be >= 0');
if (taxRate < 0 || taxRate > 1) throw new RangeError('taxRate must be in [0, 1]');
const discounted = applyDiscount(subtotal, discountCode);
return Math.floor(discounted * (1 + taxRate));
}TSDoc (TypeScript-flavoured JSDoc) is consumed by TypeDoc to generate HTML/Markdown API docs and by VS Code IntelliSense for hover documentation. The @example block is runnable by doctest tools like jest-doctest.
YARD doc comments in Ruby
# frozen_string_literal: true
module Payments
# Processes a payment charge via the Stripe API.
#
# Handles idempotency via Stripe's idempotency key mechanism
# to prevent double charges on network retries (Stripe docs: /idempotency).
# The idempotency key is derived from the order ID to ensure the same
# order cannot be charged twice, even across process restarts.
#
# @param order [Order] the order to charge; must be in :confirmed status
# @param payment_method_id [String] Stripe PaymentMethod ID (pm_...)
# @return [Stripe::PaymentIntent] the confirmed PaymentIntent object
# @raise [Stripe::CardError] if the card is declined
# @raise [ArgumentError] if order is not in :confirmed status
#
# @example
# intent = Payments::ChargeService.call(order, 'pm_1234567890')
# intent.status # => "succeeded"
class ChargeService
def self.call(order, payment_method_id)
raise ArgumentError, 'Order must be confirmed' unless order.confirmed?
Stripe::PaymentIntent.create(
{
amount: order.total_cents,
currency: order.currency,
payment_method: payment_method_id,
confirm: true
},
{ idempotency_key: "charge-order-#{order.id}" }
)
end
end
endYARD's @param and @return annotations enable yard server to generate searchable HTML docs and allow solargraph (Ruby language server) to provide type-aware autocomplete in VS Code.
Use Cases
- 01Public library APIs where users need parameter types, return values, and error conditions without reading the implementation
- 02Complex business rules embedded in methods where the reasoning (legal requirement, legacy constraint, domain quirk) is not inferrable from the code
- 03Architectural decision records (ADRs) stored alongside source code (docs/adr/) explaining why a technology or pattern was chosen
- 04Onboarding documentation: README files that cover local setup, environment variables, and the first-run experience save hours per new engineer
When Not to Use
- //Do not comment what the code obviously does: # Increment counter followed by counter += 1 is noise that decays out of sync with the code
- //Do not keep outdated comments; a comment that says "this always returns true" when the function now sometimes returns false is actively harmful
- //Do not write doc comments for private implementation details; they clutter the file and are not surfaced in generated docs anyway
Technical Notes
- TypeDoc (v0.25+) generates documentation from TSDoc comments and TypeScript types. It can be integrated into CI to publish to GitHub Pages on every merge to main
- YARD is configured via .yardopts; yard server --reload spins up a local documentation server with file watching for development
- Architecture Decision Records (Michael Nygard, 2011) are lightweight documents stored as markdown in the repo capturing context, decision, and consequences. adr-tools CLI manages the lifecycle
- Documentation drift (code changes but comments do not) is reduced by documentation tests: Rust's doctest, Python's doctest module, and jest-doctest for JavaScript run code from @example blocks as actual tests