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 (
priceAfterTaxoverfinalValue) - Use pronounceable names (
timestampoverts,customerovercust) - Avoid mental mapping (don’t use
i,j,kunless in a trivial loop) - Make meaningful distinctions (
getUserAccount()vsgetUserAccountInfo()— 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, orshould
// ❌ 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
| Prefix | Meaning | Example |
|---|---|---|
get | retrieve value | getConfig(), getUserById() |
set | assign value | setVolume(), setTheme() |
load | retrieve + store/cache | loadSettings(), loadUserData() |
fetch | remote retrieval | fetchUserData(), fetchFromAPI() |
create | make new instance | createInvoice(), createUser() |
update | modify existing | updateUser(), updateCart() |
remove/delete | destroy entity | deleteFile(), removeItem() |
render | produce output | renderPage(), renderChart() |
calculate | perform computation | calculateTotal(), calculateTax() |
handle | event or error | handleError(), handleClick() |
validate | check correctness | validateEmail(), validateForm() |
parse | convert format | parseJSON(), parseDate() |
format | transform display | formatCurrency(), formatDate() |
toggle | switch state | toggleMenu(), toggleTheme() |
init/initialize | setup | initApp(), initializeDatabase() |
Naming Cases by Use Case
| Case | Usage | Example |
|---|---|---|
kebab-case | File names | user-profile.js, invoice-list.jsx |
camelCase | Variables, functions | userName, calculateTotal() |
snake_case | JSON/object keys, database fields | user_name, created_at |
UPPER_SNAKE_CASE | Constants | MAX_ITEMS, API_KEY |
PascalCase | Classes, React components | UserAccount, 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
- Code is read far more often than it’s written — optimize for reading
- Delete code liberally — the best code is no code
- Refactor regularly — improve as you go, don’t wait for perfection
- Use tools — linters, formatters, and type checkers catch errors
- Get feedback — code reviews make everyone better
- 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.