Code Comments: Documentation or Admitting Your Code Sucks?
Every comment in your codebase is an admission of failure. Itâs you saying âI couldnât write this clearly enough the first time, so hereâs an explanation for what this mess is supposed to do.â
This isnât anti-documentation extremism. Good documentation is essential. But when you need to write a paragraph explaining what six lines of code do, the problem isnât missing documentationâitâs bad code.
Self-documenting code isnât some mythical ideal pursued by perfectionist academics. Itâs a practical skill that eliminates the need for most comments by making your code so clear that additional explanation becomes redundant. Itâs the difference between code that whispers its intentions and code that screams them.
Hereâs how to write code that doesnât need a user manual.
The Comment Smell: When Documentation Becomes a Crutch
Walk through any legacy codebase and youâll find comments like this:
// Increment i by 1
i++;
// Check if user is not null and has admin role
if (user != null && user.role === 'admin') {
// Allow access to admin panel
showAdminPanel();
}
// Loop through all products and calculate total price
let total = 0;
for (let j = 0; j < products.length; j++) {
// Add current product price to running total
total += products[j].price;
}
These comments donât add any information that isnât already obvious from reading the code. Theyâre narrative descriptions of what the code literally does, not why it does it or what business problem it solves.
Worse, they create maintenance overhead. When the code changes, the comments go stale. Now you have lying documentation that misleads future developers, including you when you return to this code in six months and canât remember what you were thinking.
Naming: The Foundation of Self-Documenting Code
The quality of your code is directly proportional to the quality of your names. Every variable, function, and class name is an opportunity to communicate intent. Waste that opportunity, and youâve created a puzzle for the next developer to solve.
Variables That Tell Their Story
Your variables are the nouns in your codeâs narrative. Make them descriptive enough that anyone can understand their purpose:
// đŠ Cryptic garbage that needs explaining
const d = 86400000; // milliseconds in a day
const u = getCurrentUser(); // current user object
const f = checkStatus(); // user verification flag
const data = api.get('/stuff'); // customer data from API
// ⨠Self-explanatory code that needs no comments
const MILLISECONDS_PER_DAY = 86400000;
const currentUser = getCurrentUser();
const isUserVerified = checkStatus();
const customerProfile = api.get('/customers/profile');
Modern IDEs have autocomplete. Thereâs no excuse for d when you could write daysSinceLastLogin. The extra keystrokes donât slow you down, but theyâll save hours of debugging later when youâre trying to figure out what d was supposed to represent.
Functions That Describe Their Purpose
Function names should describe what they do, not how they do it. If you canât tell what a function returns from its name alone, it needs a better name:
// đŠ Vague names that hide their purpose
function handle(user) {
// validate user credentials and update login timestamp
if (validateCredentials(user.email, user.password)) {
updateLastLogin(user.id);
return true;
}
return false;
}
function process(items, rate) {
// calculate total with tax applied
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 + rate);
}
// ⨠Names that reveal intent immediately
function authenticateUserAndUpdateTimestamp(user) {
if (validateCredentials(user.email, user.password)) {
updateLastLogin(user.id);
return true;
}
return false;
}
function calculateTotalWithTax(items, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 + taxRate);
}
When you read if (authenticateUserAndUpdateTimestamp(user)), you immediately understand whatâs happening without diving into implementation details or reading explanatory comments.
Boolean Variables That Ask Questions
Boolean variables should read like yes/no questions. If you need to think about what true or false means in context, the name is wrong:
// đŠ Ambiguous boolean names
const user = checkLogin(); // is this the user object or login status?
const data = validateForm(); // what does true/false mean here?
const status = connection(); // connected? connecting? disconnected?
// ⨠Obviously boolean questions
const isUserLoggedIn = checkLogin();
const hasValidFormData = validateForm();
const isConnectionActive = connection();
Structure: Code That Flows Like a Well-Written Story
Clear naming gets you halfway to self-documenting code. The other half is organizing your logic so it reads like coherent prose instead of stream-of-consciousness rambling.
Single Responsibility: One Function, One Job
Each function should do exactly one thing and do it well. If you canât describe what your function does without using âand,â itâs doing too much:
// đŠ Function trying to do everything at once
function processOrder(orderId) {
// Get order from database
const order = db.query('SELECT * FROM orders WHERE id = ?', orderId);
// Validate payment method
if (!order.paymentMethod || !isValidCard(order.paymentMethod)) {
throw new Error('Invalid payment method');
}
// Calculate shipping cost
const shippingCost = calculateShipping(order.weight, order.address);
// Update inventory
order.items.forEach(item => {
updateStock(item.productId, -item.quantity);
});
// Send confirmation email
sendEmail(order.customerEmail, 'Order confirmed', emailTemplate(order));
// Log for analytics
analytics.track('order_processed', { orderId, total: order.total });
return order;
}
// ⨠Each function has one clear responsibility
function processOrder(orderId) {
const order = fetchOrder(orderId);
validateOrderPayment(order);
const shippingCost = calculateShipping(order);
updateInventoryForOrder(order);
sendOrderConfirmation(order);
trackOrderProcessed(order);
return order;
}
function fetchOrder(orderId) {
return db.query('SELECT * FROM orders WHERE id = ?', orderId);
}
function validateOrderPayment(order) {
if (!order.paymentMethod || !isValidCard(order.paymentMethod)) {
throw new Error('Invalid payment method');
}
}
// ... additional focused functions
The refactored version reads like a high-level workflow. Each step is self-explanatory, and you can understand the entire process without reading implementation details.
Kill Magic Numbers and Cryptic Strings
Raw numbers and strings scattered throughout your code are riddles waiting to confuse future developers:
// đŠ Magic numbers and strings everywhere
if (user.accountType === 3) {
applyDiscount(order.total * 0.15);
}
if (order.status === 'PEND') {
scheduleRetry(orderId, 300000);
}
// ⨠Named constants that explain themselves
const ACCOUNT_TYPES = {
GUEST: 1,
MEMBER: 2,
PREMIUM: 3
};
const PREMIUM_DISCOUNT_RATE = 0.15;
const ORDER_STATUS = {
PENDING: 'PEND',
CONFIRMED: 'CONF',
SHIPPED: 'SHIP'
};
const RETRY_DELAY_MS = 5 * 60 * 1000; // 5 minutes
if (user.accountType === ACCOUNT_TYPES.PREMIUM) {
applyDiscount(order.total * PREMIUM_DISCOUNT_RATE);
}
if (order.status === ORDER_STATUS.PENDING) {
scheduleRetry(orderId, RETRY_DELAY_MS);
}
Flatten Nested Conditionals
Deeply nested if statements create cognitive overload. Use guard clauses to handle edge cases early and keep the main logic unindented:
// đŠ Nested pyramid of doom
function processPayment(user, paymentMethod, amount) {
if (user) {
if (user.isActive) {
if (paymentMethod) {
if (paymentMethod.isValid) {
if (amount > 0) {
// Finally, the actual logic buried under 5 levels
return chargeCard(paymentMethod, amount);
}
}
}
}
}
return { error: 'Payment failed' };
}
// ⨠Guard clauses make the happy path obvious
function processPayment(user, paymentMethod, amount) {
if (!user) {
return { error: 'User required' };
}
if (!user.isActive) {
return { error: 'Account inactive' };
}
if (!paymentMethod || !paymentMethod.isValid) {
return { error: 'Invalid payment method' };
}
if (amount <= 0) {
return { error: 'Invalid amount' };
}
// Happy path is clear and unindented
return chargeCard(paymentMethod, amount);
}
Advanced Clarity Techniques
Intention-Revealing Intermediate Variables
Break complex expressions into well-named variables that explain what youâre checking:
// đŠ Complex expression that requires mental parsing
if (user.createdAt < Date.now() - (30 * 24 * 60 * 60 * 1000) &&
user.loginCount === 0 &&
!user.emailVerified &&
user.subscriptionStatus !== 'premium') {
sendAbandonmentEmail(user);
}
// ⨠Intermediate variables that reveal intent
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
const accountAge = Date.now() - user.createdAt;
const isAccountOld = accountAge > THIRTY_DAYS_MS;
const hasNeverLoggedIn = user.loginCount === 0;
const hasUnverifiedEmail = !user.emailVerified;
const isNotPremiumUser = user.subscriptionStatus !== 'premium';
if (isAccountOld && hasNeverLoggedIn && hasUnverifiedEmail && isNotPremiumUser) {
sendAbandonmentEmail(user);
}
Extract Complex Logic Into Named Functions
Instead of comments explaining what code does, extract it into a function whose name explains the purpose:
// đŠ Comment describes what the complex logic does
function checkUserAccess(user) {
// Check if user has premium access or valid trial
if ((user.subscription === 'premium' && user.subscriptionExpiry > Date.now()) ||
(user.subscription === 'trial' && user.trialDaysLeft > 0) ||
user.hasLifetimeAccess) {
return true;
}
return false;
}
// ⨠Function name explains the business logic
function checkUserAccess(user) {
return hasValidPremiumAccess(user);
}
function hasValidPremiumAccess(user) {
return hasActivePremiumSubscription(user) ||
hasValidTrialAccess(user) ||
user.hasLifetimeAccess;
}
function hasActivePremiumSubscription(user) {
return user.subscription === 'premium' && user.subscriptionExpiry > Date.now();
}
function hasValidTrialAccess(user) {
return user.subscription === 'trial' && user.trialDaysLeft > 0;
}
When Comments Are Actually Useful (The Rare Cases)
After applying these techniques, youâll find you need far fewer comments. But some situations still warrant explanation:
Business Context and External Constraints
// Payment processor requires exactly 100 transactions per batch
// due to legacy mainframe limitations. Larger batches timeout.
// See vendor documentation: DOC-PAYMENT-BATCH-2023
const PAYMENT_BATCH_SIZE = 100;
Non-Obvious Performance or Security Decisions
// Using linear search instead of binary search because datasets
// are always <20 items. Readability trumps micro-optimization here.
function findUserInSmallTeam(users, targetId) {
return users.find(user => user.id === targetId);
}
Workarounds for External Systems
// HACK: Safari 15.x has a Date parsing bug with GMT+14 timezone
// Remove this after Safari 16.2+ adoption exceeds 95%
// Bug report: https://bugs.webkit.org/show_bug.cgi?id=12345
if (isSafari() && userTimezone === 'GMT+14') {
parsedDate = applySafariDateWorkaround(dateString);
}
The Mindset Shift
Self-documenting code requires a fundamental change in how you think about programming. Stop writing code for the compiler and start writing it for humans. The compiler will execute whatever syntactically correct garbage you feed it, but humans have to read it, understand it, debug it, and modify it.
Every line of code is a sentence in a story youâre telling. Every function is a paragraph with a clear theme. Every module is a chapter that serves the larger narrative. Your job isnât just to make it workâitâs to make it comprehensible.
When you embrace this mindset, something counterintuitive happens: your code becomes easier to write, not harder. Clear code is simple code. Simple code has fewer bugs. Fewer bugs mean less time spent debugging mysterious failures at 2 AM.
The Economics of Clear Code
Self-documenting code isnât perfectionist procrastinationâitâs practical economics. The cost of writing clear code upfront is minimal compared to the cost of maintaining unclear code over time.
Every hour you spend making code self-explanatory saves multiple hours for every future developer who touches that code. Including you, when you return to it six months later wondering what the hell you were thinking.
Code is written once but read dozens of times. Optimize for reading, not writing.
The best code doesnât need comments because it speaks for itself. When your variables tell their purpose, your functions describe their intent, and your structure reveals the workflow, documentation becomes redundant.
Thatâs not lazinessâthatâs craftsmanship.