Clean Code
01 / 01

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 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.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free