Clean Code Principles
Clean Code Principles Clean Code, popularized by Robert C. Martin ("Uncle Bob"), is a set of practices for writing code that's easy for the next person — often …
Clean Code Principles
Clean Code, popularized by Robert C. Martin ("Uncle Bob"), is a set of practices for writing code that's easy for the next person — often you, in six months — to read, understand, and safely change. Code is read far more often than it's written, so optimizing for readability isn't a nicety, it's the highest-leverage thing you can do for a codebase's long-term velocity.
Naming
A name should tell you why something exists, what it does, and how it's used — without needing a comment to explain it. If you need a comment to clarify a name, the name is the bug. Avoid noise words (data, info, manager), avoid encoding types into names, and use pronounceable, searchable names over cryptic abbreviations.
// Unclear: what is 'd'? days? a date? what unit?
function elapsed(d) {
return d * 86400000
}
// Clear: the name states the unit and the intent
function daysToMilliseconds(days) {
return days * MILLISECONDS_PER_DAY
}
// Unclear: 'list' and 'process' tell you nothing domain-specific
function process(list) {
return list.filter(x => x.s === 1)
}
// Clear: reads like the business rule it implements
function getActiveUsers(users) {
return users.filter(user => user.status === UserStatus.ACTIVE)
}
// Unclear: a boolean named without a question reads ambiguously at call sites
let open = true
if (open) { ... }
// Clear: is/has/should prefixes make booleans read naturally in conditionals
let isAccountOpen = true
if (isAccountOpen) { ... }A good test: read the name out loud without looking at its implementation. If a teammate couldn't guess roughly what it does, the name needs work — not a comment bolted on top of it.
Functions: Small & Single-Purpose
A function should do one thing, do it well, and do it only. That's not about line count for its own sake — a 40-line function that does one cohesive thing can be cleaner than five 5-line functions that fragment one idea across indirection. The real test is: can you describe what the function does without using "and"?
// Does too much: validates, computes tax, saves, AND sends an email.
// A change to any one of those four concerns forces you to re-read all of them.
async function checkout(cart, user) {
if (!cart.items.length) throw new Error('Cart is empty')
if (!user.email) throw new Error('Missing email')
let total = 0
for (const item of cart.items) {
total += item.price * item.quantity
}
const tax = total * 0.0825
const finalTotal = total + tax
const order = await db.orders.create({ userId: user.id, total: finalTotal })
await emailService.send(user.email, 'Order confirmed', `Total: $${finalTotal}`)
return order
}
// Split along the seams: each function has one reason to change
function validateCheckout(cart, user) {
if (!cart.items.length) throw new Error('Cart is empty')
if (!user.email) throw new Error('Missing email')
}
function calculateTotal(cart) {
const subtotal = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
return subtotal + subtotal * TAX_RATE
}
async function checkout(cart, user) {
validateCheckout(cart, user)
const total = calculateTotal(cart)
const order = await db.orders.create({ userId: user.id, total })
await emailService.sendOrderConfirmation(user.email, order)
return order
}Splitting like this doesn't just shorten checkout() — it makes calculateTotal() and validateCheckout() independently testable and reusable, and it means a tax-rate bug fix touches one small, obviously-relevant function instead of a 20-line function juggling four concerns.
Comments & Error Handling
The best comment is the one you didn't need to write because the code explained itself. Comments should capture the why (a non-obvious business reason, a workaround for a specific bug, an intentional trade-off) — never the what, which the code already says, and never as an apology for a bad name that a rename would fix for free.
// Bad: restates the code, adds nothing
// increment i by 1
i++
// Bad: a comment papering over an unclear name instead of fixing it
// check if user can edit (admin or owner)
function chk(u, r) {
return u.role === 'admin' || u.id === r.ownerId
}
// Good: the code is self-explanatory, no comment needed
function canEditResource(user, resource) {
return user.role === 'admin' || user.id === resource.ownerId
}
// Good: explains WHY, a fact the code alone can't convey
// Stripe webhooks can arrive out of order during retries, so we
// ignore events older than the one we've already processed.
if (event.createdAt < lastProcessedEvent.createdAt) return
// Bad error handling: swallows the failure, caller has no idea it happened
function saveUser(user) {
try {
db.users.save(user)
} catch (e) {
console.log('error')
}
}
// Good: fails loudly with context, lets the caller decide how to respond
function saveUser(user) {
try {
return db.users.save(user)
} catch (err) {
throw new Error(`Failed to save user ${user.id}: ${err.message}`, { cause: err })
}
}Treat error handling as a first-class concern, not an afterthought wrapped in a catch-and-log. Prefer exceptions (or explicit result types) over returning error codes or null, since those are easy for a caller to silently ignore — and always preserve enough context in the error to actually debug the failure later.
Code Smells & Refactoring
A code smell isn't a bug — it's a symptom that something will make future changes harder than it needs to. Duplicated logic, deeply nested conditionals, functions with more than 3-4 parameters, and classes that know too much about each other's internals are the most common ones. Clean Code is not achieved once; it's maintained through continuous small refactors as understanding of the problem grows.
// Smell: deep nesting hides the actual logic in a pyramid of conditionals
function getDiscount(user) {
if (user) {
if (user.isActive) {
if (user.orders.length > 10) {
return 0.2
} else {
return 0.1
}
} else {
return 0
}
} else {
return 0
}
}
// Refactored: guard clauses return early, flattening the logic
function getDiscount(user) {
if (!user || !user.isActive) return 0
if (user.orders.length > 10) return 0.2
return 0.1
}
// Smell: duplicated validation logic copy-pasted across three handlers
function createPost(data) {
if (!data.title || data.title.length > 200) throw new Error('Invalid title')
if (!data.body || data.body.length < 10) throw new Error('Invalid body')
// ...
}
function updatePost(data) {
if (!data.title || data.title.length > 200) throw new Error('Invalid title')
if (!data.body || data.body.length < 10) throw new Error('Invalid body')
// ...
}
// Refactored: one source of truth for the validation rule
function validatePost(data) {
if (!data.title || data.title.length > 200) throw new Error('Invalid title')
if (!data.body || data.body.length < 10) throw new Error('Invalid body')
}
function createPost(data) { validatePost(data); /* ... */ }
function updatePost(data) { validatePost(data); /* ... */ }Practical Tips & Pitfalls
Follow the Boy Scout Rule: leave code a little cleaner than you found it, even in files you're only passing through — small, continuous improvements compound.
Prefer guard clauses (early returns) over nested if/else — they eliminate a level of indentation and let the reader discard invalid cases before reasoning about the main logic.
Don't confuse "clean" with "clever" — a one-liner that requires re-reading three times is worse than five obvious lines. Optimize for the reader's cognitive load, not for line count.
Avoid magic numbers and strings — name them as constants so the value's meaning is documented at its single definition, not re-derived at every call site.
Don't over-apply DRY to the point of coupling two things that only look similar today but change for unrelated reasons — premature abstraction is its own code smell.
Refactor with a safety net: clean code and a test suite are complementary — refactoring without tests is just "changing code and hoping," not refactoring.
Clean Code is a direction, not a finish line — apply it pragmatically against the codebase's actual pain points rather than treating every rule as an absolute law in every context.