All topics
General · Learning hub

Clean Code notes for developers

Master Clean Code with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Clean Code quizMore General notes
Clean Code

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.

Keep your Clean Code knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever