Learn clean code principles for JavaScript. Meaningful naming, small functions, DRY, and best practices for maintainable code.
Why do some codebases feel like a maze while others read like a well-written story? What makes code easy to change versus code that makes you want to rewrite everything from scratch?
Copy
Ask AI
// Which would you rather debug at 2am?// Version Afunction p(a, b) { let x = 0 for (let i = 0; i < a.length; i++) { if (a[i].s === 1) x += a[i].p * b } return x}// Version Bfunction calculateActiveProductsTotal(products, taxRate) { let total = 0 for (const product of products) { if (product.status === PRODUCT_STATUS.ACTIVE) { total += product.price * taxRate } } return total}
Clean code is code that’s easy to read, easy to understand, and easy to change. The principles behind clean code were popularized by Robert C. Martin’s book Clean Code: A Handbook of Agile Software Craftsmanship, and Ryan McDermott adapted these principles specifically for JavaScript in his clean-code-javascript repository (94k+ GitHub stars). Both are essential reading for any JavaScript developer.
What you’ll learn in this guide:
What makes code “clean” and why it matters
Naming conventions that make code self-documenting
How to write small, focused functions that do one thing
The DRY principle and when to apply it
How to avoid side effects and write predictable code
Think of your code like a newspaper article. A reader should understand the gist from the headline, get more details from the first paragraph, and find supporting information as they read further. Your code should work the same way: high-level functions at the top, implementation details below.
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ CODE LIKE A NEWSPAPER │├─────────────────────────────────────────────────────────────────────────┤│ ││ // HEADLINE: What does this module do? ││ export function processUserOrder(userId, orderId) { ││ const user = getUser(userId) ││ const order = getOrder(orderId) ││ validateOrder(user, order) ││ return chargeAndShip(user, order) ││ } ││ ││ // DETAILS: How does it do it? ││ function getUser(userId) { ... } ││ function getOrder(orderId) { ... } ││ function validateOrder(user, order) { ... } ││ function chargeAndShip(user, order) { ... } ││ ││ Read top-to-bottom. The "what" comes before the "how". ││ │└─────────────────────────────────────────────────────────────────────────┘
// ❌ What does this even mean?const yyyymmdstr = moment().format('YYYY/MM/DD')const d = new Date()const t = d.getTime()// ✓ Crystal clearconst currentDate = moment().format('YYYY/MM/DD')const now = new Date()const timestamp = now.getTime()
Pick one word per concept and stick with it. If you fetch users with getUser(), don’t also have fetchClient() and retrieveCustomer().
Copy
Ask AI
// ❌ Inconsistent - which one do I use?getUserInfo()fetchClientData()retrieveCustomerRecord()// ✓ Consistent vocabularygetUser()getClient()getCustomer()
Single-letter variables force readers to remember what a, x, or l mean. Be explicit.
Copy
Ask AI
// ❌ What is 'l'? A number? A location? A letter?locations.forEach(l => { doStuff() // ... 50 lines later dispatch(l) // Wait, what was 'l' again?})// ✓ No guessing requiredlocations.forEach(location => { doStuff() dispatch(location)})
This is the single most important rule in clean code, known as the Single Responsibility Principle (SRP). As Robert C. Martin states in Clean Code, “a function should do one thing, do it well, and do it only.” When functions do one thing, they’re easier to name, easier to test, and easier to reuse.
// ❌ This function does too many thingsfunction emailClients(clients) { clients.forEach(client => { const clientRecord = database.lookup(client) if (clientRecord.isActive()) { email(client) } })}// ✓ Each function has one jobfunction emailActiveClients(clients) { clients .filter(isActiveClient) .forEach(email)}function isActiveClient(client) { const clientRecord = database.lookup(client) return clientRecord.isActive()}
Duplicate code means multiple places to update when logic changes. The DRY principle was coined by Andy Hunt and Dave Thomas in The Pragmatic Programmer, where they define it as “every piece of knowledge must have a single, unambiguous, authoritative representation.” But be careful: a bad abstraction is worse than duplication. Only abstract when you see a clear pattern.
A function has a side effect when it does something other than take inputs and return outputs: modifying a global variable, writing to a file, or mutating an input parameter. Side effects make code unpredictable and hard to test. For a deeper dive, see our Pure Functions guide.
Copy
Ask AI
// ❌ Mutates the original array - side effect!function addItemToCart(cart, item) { cart.push({ item, date: Date.now() })}// ✓ Returns a new array - no side effectsfunction addItemToCart(cart, item) { return [...cart, { item, date: Date.now() }]}
Copy
Ask AI
// ❌ Modifies global statelet name = 'Ryan McDermott'function splitName() { name = name.split(' ') // Mutates global!}// ✓ Pure function - no globals modifiedfunction splitName(name) { return name.split(' ')}const fullName = 'Ryan McDermott'const nameParts = splitName(fullName)
Deeply nested code is hard to follow. Use early returns to handle edge cases first, then write the main logic without extra indentation.
Copy
Ask AI
// ❌ Deeply nested - hard to followfunction getPayAmount(employee) { let result if (employee.isSeparated) { result = { amount: 0, reason: 'separated' } } else { if (employee.isRetired) { result = { amount: 0, reason: 'retired' } } else { // ... complex salary calculation result = { amount: salary, reason: 'employed' } } } return result}// ✓ Guard clauses - flat and readablefunction getPayAmount(employee) { if (employee.isSeparated) { return { amount: 0, reason: 'separated' } } if (employee.isRetired) { return { amount: 0, reason: 'retired' } } // Main logic at the top level const salary = calculateSalary(employee) return { amount: salary, reason: 'employed' }}
The same applies to loops. Use continue to skip iterations instead of nesting:
Copy
Ask AI
// ❌ Unnecessary nestingfor (const user of users) { if (user.isActive) { if (user.hasPermission) { processUser(user) } }}// ✓ Flat and scannablefor (const user of users) { if (!user.isActive) continue if (!user.hasPermission) continue processUser(user)}
Good code mostly documents itself. Comments should explain why, not what. According to a Stack Overflow Developer Survey, over 80% of developers consider code readability more important than clever optimization. If you need a comment to explain what code does, consider rewriting the code to be clearer.
// ❌ This is what git history is for/** * 2016-12-20: Removed monads (RM) * 2016-10-01: Added special monads (JP) * 2016-02-03: Removed type-checking (LI) */function combine(a, b) { return a + b}// ✓ Just the codefunction combine(a, b) { return a + b}
Don’t force clients to depend on methods they don’t use. In JavaScript, use optional configuration objects instead of requiring many parameters.
Copy
Ask AI
// ❌ Forcing clients to provide options they don't needclass DOMTraverser { constructor(settings) { this.settings = settings this.rootNode = settings.rootNode this.settings.animationModule.setup() // Required even if not needed! }}const traverser = new DOMTraverser({ rootNode: document.body, animationModule: { setup() {} } // Must provide even if not animating})// ✓ Make features optionalclass DOMTraverser { constructor(settings) { this.settings = settings this.rootNode = settings.rootNode if (settings.animationModule) { settings.animationModule.setup() } }}const traverser = new DOMTraverser({ rootNode: document.body // animationModule is optional now})
Dependency Inversion Principle (DIP)
Depend on abstractions, not concrete implementations. Inject dependencies rather than instantiating them inside your classes.
Copy
Ask AI
// ❌ Tightly coupled to InventoryRequesterclass InventoryTracker { constructor(items) { this.items = items this.requester = new InventoryRequester() // Hard dependency }}// ✓ Dependency injectionclass InventoryTracker { constructor(items, requester) { this.items = items this.requester = requester // Injected - can be any requester }}
Functions that do one thing with no side effects are easy to test. If a function is hard to test, it’s often a sign that it’s doing too much or has hidden dependencies. Clean code and testable code go hand in hand.
Answer:The name process is too vague. It doesn’t tell you what kind of processing happens or what kind of data is expected. Better names would be validateUserInput, parseJsonResponse, or calculateOrderTotal, depending on what the function actually does.
Question 2: Why is this function problematic?
Copy
Ask AI
function createUser(name, email, age, isAdmin, sendWelcomeEmail) { // ...}
Answer:Too many parameters (5). It’s hard to remember the order, and the boolean flags (isAdmin, sendWelcomeEmail) suggest the function might be doing multiple things. Refactor to use an options object:
Copy
Ask AI
function createUser({ name, email, age, isAdmin = false }) { // ...}function sendWelcomeEmail(user) { // Separate function for separate concern}
Question 3: When should you write a comment?
Answer:Write comments when you need to explain why something is done a certain way, especially for:
Business logic that isn’t obvious from the code
Workarounds for bugs or edge cases
Legal or licensing requirements
Complex algorithms where the approach isn’t self-evident
Don’t write comments that explain what the code does. If the code needs explanation, rewrite it to be clearer.
Question 4: What's a 'magic number' and why is it bad?
Answer:A magic number is an unexplained numeric literal in code, like 86400000 or 18. They’re bad because:
You can’t search for what they mean
They don’t explain their purpose
If the value needs to change, you have to find every occurrence
Replace with named constants: MILLISECONDS_PER_DAY or MINIMUM_LEGAL_AGE.
Question 5: How would you refactor this nested code?
Copy
Ask AI
function processUser(user) { if (user) { if (user.isActive) { if (user.hasPermission) { return doSomething(user) } } } return null}
Answer:Use guard clauses (early returns) to flatten the nesting:
Copy
Ask AI
function processUser(user) { if (!user) return null if (!user.isActive) return null if (!user.hasPermission) return null return doSomething(user)}
Each guard clause handles one edge case, and the main logic sits at the top level without indentation.
Clean code is code that is easy to read, easy to understand, and easy to change. Robert C. Martin defines it as code that “reads like well-written prose” in his book Clean Code. In JavaScript specifically, this means using meaningful variable names, small focused functions, consistent formatting, and avoiding side effects where possible.
How long should a JavaScript function be?
A function should do one thing and do it well. While there is no strict line limit, Robert C. Martin recommends functions rarely exceed 20 lines. If a function needs a comment to explain a section, that section should likely be its own function. The key metric is not line count but whether the function operates at a single level of abstraction.
When should I write comments in my code?
Write comments to explain why, never what. Good code is self-documenting through meaningful names and clear structure. Comments are valuable for explaining business rules, performance trade-offs, workarounds for known issues, and legal or regulatory requirements. As the saying in The Pragmatic Programmer goes, “don’t document bad code — rewrite it.”
What is the DRY principle and when should I apply it?
DRY stands for “Don’t Repeat Yourself,” coined by Andy Hunt and Dave Thomas. It means every piece of knowledge should have a single authoritative representation. Apply it when you see the same logic repeated three or more times. However, premature abstraction is worse than duplication — wait until you see a clear, stable pattern before extracting shared code.
How do I write self-documenting JavaScript code?
Use descriptive variable and function names that reveal intent (calculateTotalPrice not calc). Extract complex conditions into named boolean variables. Use const by default and let only when reassignment is needed. Prefer for...of over index-based loops, and use destructuring to name what you’re extracting. These practices reduce the need for comments.