Back to posts
14 min read

My Personal Code Readability Guideline

Philosophy

Readable code is code that explains itself.

I realized that I don’t have a personal written standard that defines what readable code means to me. Across different projects, my code sometimes lacks consistency. It’s easy to become overly focused on best practices and performance, and forget about readability — which then makes re-reading the code unnecessarily difficult.

This guide is meant to serve as my personal reference for writing easy-to-read and self-documenting code. The principles here are curated from established references and refined through my daily code writing experience using JavaScript & Typescript programming language.

Purpose

✅ To make code understandable at a glance
✅ To minimize mental load for both current and future readers
✅ To value clarity over cleverness or brevity
✅ To reduce bugs through better code organization
✅ To accelerate onboarding for new team members


Naming

General Rules

Names must reveal intent, not implementation. Before naming, ask yourself:

  • Why does it exist?
  • What does it do?
  • How is it used?

Key principles:

  • Avoid abbreviations unless they are universally known (URL, ID, API, HTML, CSS)
  • Prefer full words to save time in the future, not characters today
  • Prefer specific names per context (priceAfterTax over finalValue)
  • Use pronounceable names (timestamp over ts, customer over cust)
  • Avoid mental mapping (don’t use i, j, k unless in a trivial loop)
  • Make meaningful distinctions (getUserAccount() vs getUserAccountInfo() — what’s the difference?)

Variables

  • Use nouns for values and data (user, invoiceList, config)
  • Use plural for collections (users, invoices, products)
  • Booleans should start with is, has, can, or should
// ❌ Avoid
const active = true; // unclear if it's a state or action
const permission = checkUserRole(); // what type is this?

// ✅ Good
const isActive = true;
const hasPermission = checkUserRole();
const canEdit = user.role === "admin";
const shouldRefresh = lastUpdate < Date.now() - 5000;
  • Constants are written in UPPER_SNAKE_CASE
const MAX_RETRY_ATTEMPTS = 3;
const API_BASE_URL = "https://api.example.com";

Classes

  • Use nouns — classes represent things or concepts, not processes
// ✅ Good
class UserAccount {}
class InvoiceProcessor {}
class PaymentGateway {}

// ❌ Avoid verbs
class ProcessUser {} // sounds like a function
class HandlePayment {}

Functions and Methods

Use verbs for actions (getUser, calculateTotal, validateInput). Name functions for what they do or return, not how they do it.

Common Verb Prefixes

PrefixMeaningExample
getretrieve valuegetConfig(), getUserById()
setassign valuesetVolume(), setTheme()
loadretrieve + store/cacheloadSettings(), loadUserData()
fetchremote retrievalfetchUserData(), fetchFromAPI()
createmake new instancecreateInvoice(), createUser()
updatemodify existingupdateUser(), updateCart()
remove/deletedestroy entitydeleteFile(), removeItem()
renderproduce outputrenderPage(), renderChart()
calculateperform computationcalculateTotal(), calculateTax()
handleevent or errorhandleError(), handleClick()
validatecheck correctnessvalidateEmail(), validateForm()
parseconvert formatparseJSON(), parseDate()
formattransform displayformatCurrency(), formatDate()
toggleswitch statetoggleMenu(), toggleTheme()
init/initializesetupinitApp(), initializeDatabase()

Naming Cases by Use Case

CaseUsageExample
kebab-caseFile namesuser-profile.js, invoice-list.jsx
camelCaseVariables, functionsuserName, calculateTotal()
snake_caseJSON/object keys, database fieldsuser_name, created_at
UPPER_SNAKE_CASEConstantsMAX_ITEMS, API_KEY
PascalCaseClasses, React componentsUserAccount, NavBar

Structure and Flow

Function Design

Single Responsibility Principle

Each function should do one thing only. If naming becomes difficult, the function is likely too complex.

// ❌ Not good — does two things at once
function sumAndMultiplyByTwo(a, b) {
  return (a + b) * 2;
}

// ✅ Better — each function handles one task
function sum(a, b) {
  return a + b;
}

function multiplyByTwo(num) {
  return num * 2;
}

const result = multiplyByTwo(sum(2, 3));

Function Length

Keep functions short and focused — ideally no longer than what can fit fully on a screen without scrolling (roughly 20-30 lines max).

// ❌ Not good — too long and does too many things
function processCheckout(cart, user) {
  if (!user) throw new Error("No user found");
  if (!cart.length) throw new Error("Cart is empty");

  // 1. Calculate total
  let total = 0;
  for (const item of cart) {
    total += item.price * item.qty;
  }

  // 2. Apply discounts
  if (user.isMember) {
    total *= 0.9;
  }

  // 3. Save order
  const orderId = saveOrder({ userId: user.id, total, cart });

  // 4. Send notification
  sendEmail(user.email, `Your order ${orderId} has been placed`);

  // 5. Update analytics
  updatePurchaseStats(user.id, total);

  return orderId;
}

// ✅ Better — each step extracted into a self-explanatory function
function processCheckout(cart, user) {
  validateCheckoutRequirements(cart, user);

  const total = calculateCartTotal(cart, user);
  const orderId = saveOrder({ userId: user.id, total, cart });

  notifyUserOrderPlaced(user, orderId);
  trackPurchase(user.id, total);

  return orderId;
}

function validateCheckoutRequirements(cart, user) {
  if (!user) throw new Error("No user found");
  if (!cart.length) throw new Error("Cart is empty");
}

Top-Down Organization

Place related functions near each other and arrange them in a top-down flow, reflecting their order of use. This creates a natural reading pattern.

A top-down structure mirrors how humans read code—starting from intent → details—so readers understand the “story” before diving into implementation.

// ❌ Scattered, bottom-up, hard to read
function fetchUser() {
  /* ... */
}

function showError() {
  /* ... */
}

function handleLogin() {
  const user = fetchUser();
  if (!user) return showError();
  saveSession(user);
  redirectToDashboard();
}

function redirectToDashboard() {
  /* ... */
}
function saveSession() {
  /* ... */
}

// ✅ Top-down, related, easy to follow
function handleLogin() {
  const user = fetchUser();
  if (!user) return showError();

  saveSession(user);
  redirectToDashboard();
}

// Supporting functions (in order of use)
function fetchUser() {
  /* ... */
}
function showError() {
  /* ... */
}
function saveSession(user) {
  /* ... */
}
function redirectToDashboard() {
  /* ... */
}

Variable Scope

Define variables near their usage to minimize cognitive distance between context and action.

// ❌ Variables declared far from usage
function processOrder(order) {
  const userId = order.userId;
  const orderDate = order.date;
  const items = order.items;

  validateOrder(order);

  // ... 50 lines of code ...

  // Now using userId, but it's far from declaration
  const user = getUserById(userId);
  sendConfirmation(user);
}

// ✅ Variables declared near usage
function processOrder(order) {
  validateOrder(order);

  // ... processing logic ...

  const userId = order.userId;
  const user = getUserById(userId);
  sendConfirmation(user);
}

Intent Over Implementation

Write functions whose names state intent rather than implementation — the reader should know what it does without reading how.

Intent-based naming hides unnecessary details and makes functions resilient to internal changes. If you change the implementation later, the name still holds true.

// ❌ Implementation-focused naming
function filterUsersWhereActiveIsTrue(users) {
  return users.filter((u) => u.active === true);
}

function loopThroughArrayAndSum(numbers) {
  return numbers.reduce((sum, n) => sum + n, 0);
}

// ✅ Intent-focused naming
function getActiveUsers(users) {
  return users.filter((u) => u.active);
}

function calculateTotal(numbers) {
  return numbers.reduce((sum, n) => sum + n, 0);
}

// ❌ Describes mechanism, not purpose
function incrementByOne(number) {
  return number + 1;
}

// ✅ Describes meaning in context
function getNextIndex(currentIndex) {
  return currentIndex + 1;
}

Rule: If you can rewrite the internals without changing the function name, you named it well.


Comments

Write code so it doesn’t need comments.
Use comments to explain intent, reasoning, constraints, and trade-offs—not to restate what the code already does.

If a comment explains what the code does, refactor the code.
If a comment explains why the code must be this way, keep it.

When to Comment

Comments must focus on context, intent, and rationale, especially when the reasoning is not obvious from code alone.

// ❌ Redundant — the code already says this
i++; // Increment i by 1

const userName = "John"; // Set user name to John

// ✅ Meaningful — explains reasoning or context
// 30-day cache to reduce API cost while keeping data reasonably fresh
const CACHE_DURATION = 30 * 24 * 60 * 60 * 1000;

// Timeout must exceed serverless cold start (≈3s)
const REQUEST_TIMEOUT = 5000;

What to Comment

  • Business rules / domain decisions (why this logic matters)
  • Workarounds & hacks (include source of limitation)
  • Non-obvious algorithms or optimizations
  • External constraints (API limits, browser quirks)
  • TODOs with context and timeline
// TODO(Q2 2025): Replace polling with WebSocket once backend supports streams
function pollForUpdates() {
  setInterval(fetchLatestData, 5000);
}

// Workaround: Safari lacks support for :has() selector
if (isSafari()) {
  applyManualStyling();
}

What NOT to Comment

  • Comments that describe the obvious
  • Repeating variable names in prose
  • Commented-out code (use version control)
  • Comments that fall out of sync (delete instead of preserving history)
// ❌ Bad
// Check if user is admin
if (user.role === "admin") {
}

// ✅ Better — make the code self-explanatory
const isAdmin = user.role === "admin";
if (isAdmin) {
}

Style & Formatting

  • Use short inline comments for brief clarifications
  • Use block comments for multi-line explanations or decisions
  • Keep comments as close to the relevant code as possible
// Avoid double-fetch; handler triggers automatically on mount
useEffect(() => {
  fetchData();
}, []);

Code Organization

File Structure

Use a modular, feature-oriented structure inspired by Bulletproof React (this redability guide is not about React per-se, but I love to put this here :)).

Reference: https://github.com/alan2207/bulletproof-react


Error Handling

Be Specific

Use descriptive error messages that help debugging.

// ❌ Vague
throw new Error("Invalid input");

// ✅ Specific
throw new Error("Email format is invalid. Expected format: user@domain.com");
throw new Error(`User ID ${userId} not found in database`);

Fail Fast

Validate inputs early and exit quickly.

// ✅ Guard clauses at the top
function updateUser(userId, data) {
  if (!userId) throw new Error("User ID is required");
  if (!data) throw new Error("Update data is required");
  if (!data.email) throw new Error("Email is required");

  // Main logic here
  return performUpdate(userId, data);
}

Conditionals

Positive Conditionals

Use positive conditions when possible — they’re easier to read.

// ❌ Double negative
if (!isNotValid) {
}

// ✅ Positive
if (isValid) {
}

Early Returns

Reduce nesting with early returns (guard clauses).

// ❌ Deep nesting
function processUser(user) {
  if (user) {
    if (user.isActive) {
      if (user.hasPermission) {
        // do something
      }
    }
  }
}

// ✅ Early returns — flat and clear
function processUser(user) {
  if (!user) return;
  if (!user.isActive) return;
  if (!user.hasPermission) return;

  // do something
}

Extract Complex Conditions

Give complex conditions meaningful names.

// ❌ Hard to understand at a glance
if (user.age >= 18 && user.hasAccount && user.agreedToTerms && !user.isBanned) {
  allowAccess();
}

// ✅ Self-documenting
const canAccessPlatform =
  user.age >= 18 && user.hasAccount && user.agreedToTerms && !user.isBanned;

if (canAccessPlatform) {
  allowAccess();
}

Use Look up tables

Replace repetitive if/else or switch chains with maps or lookup tables.

// ❌ Long if/else chain
function getStatusLabel(status) {
  if (status === "active") return "Active";
  else if (status === "pending") return "Pending";
  else if (status === "inactive") return "Inactive";
  else if (status === "suspended") return "Suspended";
  else return "Unknown";
}

// ❌ Long switch statement
function getStatusColor(status) {
  switch (status) {
    case "active":
      return "green";
    case "pending":
      return "yellow";
    case "inactive":
      return "gray";
    case "suspended":
      return "red";
    default:
      return "black";
  }
}

// ✅ Lookup table — cleaner and easier to maintain
const STATUS_LABELS = {
  active: "Active",
  pending: "Pending",
  inactive: "Inactive",
  suspended: "Suspended",
};

const STATUS_COLORS = {
  active: "green",
  pending: "yellow",
  inactive: "gray",
  suspended: "red",
};

function getStatusLabel(status) {
  return STATUS_LABELS[status] || "Unknown";
}

function getStatusColor(status) {
  return STATUS_COLORS[status] || "black";
}

Flow of Execution

Happy Path First

The main logic (the “happy path”) should be the most visible and uninterrupted path in your code. Error handling and edge cases should branch off, not dominate.

// ❌ Happy path buried in else blocks
function processOrder(order) {
  if (!order) {
    logError("No order provided");
    return null;
  } else {
    if (!order.items.length) {
      logError("Empty order");
      return null;
    } else {
      if (!order.paymentMethod) {
        logError("No payment method");
        return null;
      } else {
        // Finally, the actual logic
        const total = calculateTotal(order);
        chargePayment(order.paymentMethod, total);
        return createConfirmation(order);
      }
    }
  }
}

// ✅ Happy path is clear and uninterrupted
function processOrder(order) {
  if (!order) {
    logError("No order provided");
    return null;
  }

  if (!order.items.length) {
    logError("Empty order");
    return null;
  }

  if (!order.paymentMethod) {
    logError("No payment method");
    return null;
  }

  // Happy path — clear and prominent
  const total = calculateTotal(order);
  chargePayment(order.paymentMethod, total);
  return createConfirmation(order);
}

Logical Top-to-Bottom Flow

Structure the code so it reads like a story — each section handling one concern before moving to the next. Avoid jumping back and forth.

// ❌ Spaghetti flow — concerns mixed together
function handleFormSubmit(formData) {
  let result;
  const errors = [];

  if (formData.email) {
    result = { email: formData.email };
  }

  if (!formData.name) {
    errors.push("Name required");
  }

  if (formData.name) {
    result.name = formData.name;
  }

  if (!formData.email) {
    errors.push("Email required");
  }

  if (errors.length) {
    return { success: false, errors };
  }

  return saveUser(result);
}

// ✅ Clear flow: validate → transform → save
function handleFormSubmit(formData) {
  // 1. Validation
  const errors = validateFormData(formData);
  if (errors.length) {
    return { success: false, errors };
  }

  // 2. Transformation
  const userData = {
    name: formData.name,
    email: formData.email,
  };

  // 3. Persistence
  return saveUser(userData);
}

function validateFormData(formData) {
  const errors = [];
  if (!formData.name) errors.push("Name required");
  if (!formData.email) errors.push("Email required");
  return errors;
}

Separate Concerns Vertically

Inputs, transformations, and outputs should appear in order. This creates a natural reading flow.

// ✅ Clear vertical separation of concerns
async function generateReport(userId, dateRange) {
  // --- Input / Data Retrieval ---
  const user = await fetchUser(userId);
  const transactions = await fetchTransactions(userId, dateRange);
  const exchangeRates = await fetchExchangeRates();

  // --- Transformation / Processing ---
  const normalizedTransactions = normalizeToUSD(transactions, exchangeRates);
  const summary = calculateSummary(normalizedTransactions);
  const insights = generateInsights(summary, user.preferences);

  // --- Output / Delivery ---
  const report = formatReport(summary, insights);
  await saveReport(userId, report);
  await notifyUser(user.email, report.id);

  return report;
}

Keep Indentation Shallow

If you find yourself beyond 2–3 levels of indentation, refactor into smaller functions or use early returns.

// ❌ Too deep — hard to follow
function processItems(items) {
  if (items) {
    for (const item of items) {
      if (item.active) {
        if (item.price > 0) {
          if (item.inStock) {
            // Finally doing something at 4 levels deep
            addToCart(item);
          }
        }
      }
    }
  }
}

// ✅ Shallow and clear
function processItems(items) {
  if (!items) return;

  for (const item of items) {
    if (shouldAddToCart(item)) {
      addToCart(item);
    }
  }
}

function shouldAddToCart(item) {
  return item.active && item.price > 0 && item.inStock;
}

Data Structures

Use Meaningful Structures

Choose data structures that make intent clear.

// ❌ Magic numbers in an array
const user = ["John", 25, "admin"];
const name = user[0]; // What is index 0?

// ✅ Named properties
const user = {
  name: "John",
  age: 25,
  role: "admin",
};
const name = user.name; // Clear and obvious

Avoid Magic Numbers

Give numbers meaningful names.

// ❌ Magic numbers
if (user.status === 1) {
}
setTimeout(doSomething, 86400000);

// ✅ Named constants
const USER_STATUS_ACTIVE = 1;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;

if (user.status === USER_STATUS_ACTIVE) {
}
setTimeout(doSomething, ONE_DAY_MS);

Consistency

Pick One Style

Consistency matters more than the specific choice. Once you decide on a pattern, stick with it.

// ❌ Inconsistent
function getUser() {}
const fetchProduct = () => {};
function retrieve_order() {}

// ✅ Consistent
function getUser() {}
function getProduct() {}
function getOrder() {}

Use Linters and Formatters

Automate consistency with tools:

  • ESLint for code quality rules
  • Prettier for formatting
  • EditorConfig for cross-editor consistency

Visual and Spatial Patterns

Whitespace as Communication

Humans scan code spatially before reading symbols. Use whitespace strategically to signal separation and group related code.

// ❌ No visual breathing room
function processPayment(order, user, paymentMethod) {
  const total = calculateTotal(order);
  const tax = total * 0.1;
  const finalAmount = total + tax;
  validatePaymentMethod(paymentMethod);
  const transaction = chargeCard(paymentMethod, finalAmount);
  updateOrderStatus(order.id, "paid");
  sendReceipt(user.email, transaction);
  return transaction;
}

// ✅ Whitespace groups related operations
function processPayment(order, user, paymentMethod) {
  // Calculate amounts
  const total = calculateTotal(order);
  const tax = total * 0.1;
  const finalAmount = total + tax;

  // Process payment
  validatePaymentMethod(paymentMethod);
  const transaction = chargeCard(paymentMethod, finalAmount);

  // Update records and notify
  updateOrderStatus(order.id, "paid");
  sendReceipt(user.email, transaction);

  return transaction;
}

Use Blank Lines to Separate Concepts

Blank lines act as paragraph breaks. Use them to separate logical chunks within a function.

// ✅ Blank lines separate distinct operations
async function initializeApp() {
  // Configuration
  const config = loadConfig();
  validateConfig(config);

  // Database connection
  const db = await connectDatabase(config.dbUrl);
  await runMigrations(db);

  // Service initialization
  const cache = initializeCache(config.cacheSize);
  const queue = initializeJobQueue(config.queueUrl);

  // Start server
  const server = createServer({ db, cache, queue });
  await server.listen(config.port);

  console.log(`Server running on port ${config.port}`);
}

Consistent Indentation and Spacing

Your structure should communicate intent visually. Inconsistent formatting creates noise.

// ❌ Inconsistent — creates visual noise
function calculate(a, b) {
  const sum = a + b;
  const product = a * b;
  const diff = a - b;
  return { sum, product, diff };
}

// ✅ Consistent — easy to scan
function calculate(a, b) {
  const sum = a + b;
  const product = a * b;
  const diff = a - b;

  return { sum, product, diff };
}

Vertical Density

Balance between too cramped and too spread out. Code that’s too dense is hard to read; code that’s too sparse loses context.

// ❌ Too dense — hard to parse
function getUser(id) {
  if (!id) return null;
  const user = db.find(id);
  if (!user) throw new Error("Not found");
  return user;
}

// ❌ Too sparse — loses context
function getUser(id) {
  if (!id) {
    return null;
  }

  const user = db.find(id);

  if (!user) {
    throw new Error("Not found");
  }

  return user;
}

// ✅ Balanced — clear and contextual
function getUser(id) {
  if (!id) return null;

  const user = db.find(id);
  if (!user) throw new Error("Not found");

  return user;
}

Final Principles

  1. Code is read far more often than it’s written — optimize for reading
  2. Delete code liberally — the best code is no code
  3. Refactor regularly — improve as you go, don’t wait for perfection
  4. Use tools — linters, formatters, and type checkers catch errors
  5. Get feedback — code reviews make everyone better
  6. When in doubt, simplify — complexity is the enemy of readability

Change log

This guide is a living document. Will be updated as I learn and grow.