How to Write Clean Code and Best Practices

Updated: March 27, 2026

How to Write Clean Code and Best Practices

TL;DR

Clean code is readable, maintainable code with meaningful names, small functions, proper error handling, and comprehensive tests. Use modern linting tools (ESLint, Ruff, Biome), automated formatting (Prettier), and code review practices to enforce cleanliness across your team and projects.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Robert C. Martin's Clean Code remains influential, but 2026 development adds new dimensions: code written for AI assistance, enforced by modern linters, formatted automatically, and reviewed collaboratively.

Clean code isn't an aesthetic preference — it's a business decision. Teams that write clean code spend less time debugging, ship features faster, and experience fewer production incidents. In this post, we'll explore timeless principles from Clean Code, modern critiques, tools for enforcement, and practical techniques for improving code quality in your current projects.

Principles of Clean Code (Beyond the Obvious)

Meaningful Names

Bad names force readers to decipher code intent:

// Bad: What does "d" mean? How many days?
const d = new Date();
const diff = today - d;

// Good: Names explain purpose
const accountCreationDate = new Date();
const daysSinceAccountCreation = today - accountCreationDate;

Names should answer:

  • What does this variable/function represent?
  • Why does it exist?
  • How should it be used?

A good variable name saves hours of future debugging.

Small Functions with Single Responsibility

Functions that do one thing well are easier to test, reuse, and reason about:

// Bad: Does multiple things (fetch, parse, transform, cache)
async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const json = await response.json();
  const transformed = {
    id: json.id,
    fullName: `${json.first} ${json.last}`,
    email: json.email_address,
  };
  cache.set(userId, transformed);
  return transformed;
}

// Better: Separated concerns
async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

function transformUserData(rawUser) {
  return {
    id: rawUser.id,
    fullName: `${rawUser.first} ${rawUser.last}`,
    email: rawUser.email_address,
  };
}

async function getUserDataCached(userId) {
  if (cache.has(userId)) return cache.get(userId);
  const rawUser = await fetchUser(userId);
  const user = transformUserData(rawUser);
  cache.set(userId, user);
  return user;
}

The rule: If your function description includes "and," it probably violates single responsibility.

Error Handling is Part of Clean Code

Silently swallowing errors or using vague error messages creates untraceable bugs:

// Bad: Silent failure
function parseConfig(json) {
  try {
    return JSON.parse(json);
  } catch (e) {
    return null;  // What went wrong? Silent failure = nightmare
  }
}

// Better: Clear error messages and types
function parseConfig(json) {
  try {
    if (!json || typeof json !== 'string') {
      throw new Error('Config must be a non-empty string');
    }
    return JSON.parse(json);
  } catch (e) {
    if (e instanceof SyntaxError) {
      throw new Error(`Invalid JSON in config: ${e.message}`);
    }
    throw e;  // Re-throw unexpected errors
  }
}

Good error handling:

  • Provides context about what failed and why
  • Distinguishes between recoverable and fatal errors
  • Logs errors in production for monitoring

Comments Shouldn't Explain Bad Code

// Bad: Comment explains unclear logic
// Get the last character and check if it's a period
const hasPeriod = text[text.length - 1] === '.';

// Good: Clear code + comment explains "why," not "what"
const hasPeriod = endsWithPeriod(text);  // Sentences end with periods for grammar

function endsWithPeriod(text) {
  return text[text.length - 1] === '.';
}

Comments should explain why decisions were made, not what the code does. If you need comments to explain the code, the code is too unclear.

Modern Critique: When Clean Code Goes Too Far

Clean Code principles are widely respected, but some critique has emerged:

Over-abstraction: Small, single-purpose functions are good, but extracting every conditional into a separate function creates indirection without clarity.

// Over-abstracted: Too many tiny functions make the main logic hard to follow
function isValidEmail(email) {
  return hasAtSymbol(email) && hasValidDomain(email) && isNotTooLong(email);
}
const hasAtSymbol = (e) => e.includes('@');
const hasValidDomain = (e) => e.split('@')[1]?.includes('.');
const isNotTooLong = (e) => e.length <= 254;

// Pragmatic: Keep related logic together
function isValidEmail(email) {
  // RFC 5321 — max length is 254 chars
  if (email.length > 254) return false;
  // Must contain @ and domain with dot
  const [local, domain] = email.split('@');
  return local && domain && domain.includes('.');
}

Context matters: A 2-person startup building a prototype can prioritize speed over perfect abstraction. A 200-person company with legacy code needs stronger guardrails.

The modern take: Clean code principles are guidelines, not absolute rules. Adapt them to your context.

Code Readability for AI Tools

In 2026, code is increasingly read by AI (for autocomplete, code review, or analysis). This adds a new dimension to clean code:

AI-readable code characteristics:

  • Explicit intent: AI can infer purpose from clear naming and structure
  • Type annotations: Python type hints and TypeScript types help AI understand expected inputs/outputs
  • Documented APIs: Docstrings and comments for public functions
  • Consistent patterns: Regular code structure aids pattern recognition

Example with TypeScript (AI-friendly):

/** Calculates subscription discount based on term length */
function calculateDiscount(termMonths: number): number {
  if (termMonths < 3) return 0;
  if (termMonths < 12) return 0.1;  // 10% for 3-12 months
  return 0.2;  // 20% for 12+ months
}

/** Applies annual discount to subscription tier */
interface PricingTier {
  name: string;
  basePriceMonthly: number;
}

function applyDiscount(tier: PricingTier, termMonths: number): number {
  const discount = calculateDiscount(termMonths);
  return tier.basePriceMonthly * 12 * (1 - discount);
}

The type annotations and explicit function purposes make it easy for both humans and AI to understand the code logic.

Linting and Formatting Tools

Clean code is enforced by tools, not willpower. Modern toolchains automatically fix style issues and catch bugs:

JavaScript/TypeScript: ESLint

ESLint 9+ (April 5, 2024) introduced the flat config format:

// eslint.config.js
import js from '@eslint/js';
import eslintPluginReact from 'eslint-plugin-react';

export default [
  js.configs.recommended,
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: 'module',
    },
    plugins: {
      react: eslintPluginReact,
    },
    rules: {
      'no-unused-vars': 'error',
      'no-console': 'warn',
      'prefer-const': 'error',
    },
  },
];

Key ESLint rules for clean code:

  • no-unused-vars: Catch dead code
  • no-implicit-any (with TypeScript): Force type safety
  • prefer-const: Prefer immutability
  • complexity: Functions shouldn't be too complex
  • max-depth: Avoid deeply nested logic

Formatting: Prettier

Prettier removes style debates by auto-formatting code:

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2
}

Prettier + ESLint workflow:

  1. ESLint catches logic errors and bad patterns
  2. Prettier auto-fixes formatting
  3. Code review focuses on architecture and logic, not style

Python: Ruff and Pylint

Ruff is a modern, fast Python linter:

ruff check .          # Find issues
ruff format .         # Auto-format

Configuration in pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "W"]  # Errors, pyflakes, isort, warnings
ignore = ["E501"]  # Ignore line-too-long (formatter handles it)

Modern Bundler: Biome

Biome (2024+) unifies linting and formatting for JavaScript:

{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "formatter": {
    "enabled": true,
    "indentWidth": 2
  }
}

Biome is significantly faster than separate ESLint + Prettier.

Code Review as Clean Code Enforcement

Code review catches issues that automated tools miss and spreads knowledge:

Effective code review practices:

  1. Focus on logic and design, not style: Tools should enforce style; reviewers evaluate correctness.

  2. Ask questions instead of demanding changes:

    • Bad: "This function is too long, break it up"
    • Good: "What would this look like if we split the data fetching from the formatting?"
  3. Catch patterns early: Review code as it's written, not in massive PRs.

  4. Approve quickly on small PRs: Don't be the bottleneck. Small, focused changes review faster and merge faster.

  5. Document decision-making: Why was this design chosen? Include links to architecture docs or decision records.

Testing as Clean Code

Code without tests is inherently unclear about its intended behavior:

// Without tests, what should this function do exactly?
function mergeUsers(user1, user2) {
  return { ...user1, ...user2 };
}

// With tests, behavior is documented
describe('mergeUsers', () => {
  it('combines two users, with user2 properties taking precedence', () => {
    const user1 = { id: 1, name: 'Alice', email: 'alice@example.com' };
    const user2 = { name: 'Alice Updated', email: 'newemail@example.com' };
    expect(mergeUsers(user1, user2)).toEqual({
      id: 1,
      name: 'Alice Updated',
      email: 'newemail@example.com',
    });
  });

  it('preserves properties from user1 that user2 does not override', () => {
    const user1 = { id: 1, name: 'Alice', email: 'alice@example.com', verified: true };
    const user2 = { name: 'Alice Updated' };
    expect(mergeUsers(user1, user2)).toEqual({
      id: 1,
      name: 'Alice Updated',
      email: 'alice@example.com',
      verified: true,
    });
  });
});

Test-driven development (TDD) forces you to think about expected behavior before writing implementation, naturally leading to cleaner code.

Practical Steps to Improve Code Quality Today

  1. Add ESLint/Ruff to your project (even existing projects benefit)
  2. Configure Prettier and integrate with your editor for on-save formatting
  3. Enable pre-commit hooks (husky + lint-staged) to catch issues before commits
  4. Establish code review standards in your team (use PR templates)
  5. Write tests for new code — start with happy path, then edge cases
  6. Refactor incrementally — pick one function per day to improve
  7. Read and discuss code actively — code review is a learning opportunity

Conclusion

Clean code isn't about perfection — it's about sustainability. Code is read far more often than it's written. Investing in readability pays dividends through faster debugging, easier feature development, and reduced maintenance burden.

Use modern tools (ESLint, Prettier, Ruff) to automate style enforcement, rely on type systems to catch bugs early, establish code review practices for knowledge sharing, and write tests to document intended behavior. Combine these practices with timeless principles from Clean Code, and you'll build systems that remain maintainable for years.


FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.