All Posts
Clean CodeSoftware EngineeringBest PracticesRefactoring

The Art of Writing Clean Code: Principles That Actually Matter

Clean code is not about aesthetics — it is about respect for the next person who reads your code (often future you). Here are the principles worth internalizing.

The Art of Writing Clean Code: Principles That Actually Matter

I once spent three days debugging a function that was 400 lines long, had six levels of nesting, used single-letter variable names, and had not a single comment. The function worked perfectly when it was written. But six months later, no one could touch it without breaking something.

Clean code is not a style preference. It is a professional obligation. Here is what actually matters.

Naming Is the Foundation

The most powerful refactoring you can do costs nothing: rename things until they are honest.

Variables Should Reveal Intent

// What is d? What does it measure?
const d = 86400;

// Now it is obvious
const SECONDS_IN_A_DAY = 86400;

// What is the flag for?
let f = false;

// Clear
let isUserAuthenticated = false;

Functions Should Do One Thing and Be Named After That One Thing

// What does this function do? Too many things to tell from the name.
function processUser(user: User): void {
  validateEmail(user.email);
  hashPassword(user.password);
  saveToDatabase(user);
  sendWelcomeEmail(user.email);
  logAuditEvent("user_created", user.id);
}

// Better: each function has a single, named responsibility
async function registerUser(user: NewUser): Promise<User> {
  const validated = validateRegistrationData(user);
  const hashed = await hashPassword(validated.password);
  const saved = await saveUser({ ...validated, password: hashed });
  await sendWelcomeEmail(saved.email);
  await logAuditEvent("user_registered", saved.id);
  return saved;
}

Avoid Noise Words and Redundant Context

// Noise words add nothing
const userData = { name: "Jay" };  // "Data" suffix is noise
const userInfo = { name: "Jay" };  // "Info" suffix is noise

// Redundant context
class UserAccount {
  userAccountName: string;  // "userAccount" prefix is redundant
  name: string;             // Better — the class provides context
}

Functions Should Be Small

The ideal function fits on a screen without scrolling. If you need to scroll to read a function, it is a candidate for extraction.

A good rule of thumb: if you can describe what a function does without using the word "and," it is probably the right size.

// Bad: does too many things
"This function validates the form AND checks the API AND updates the state AND shows a toast"

// Good: focused
"This function validates the registration form fields"
"This function submits the registration data to the API"
"This function handles the post-registration UI state"

Avoid Deep Nesting with Early Returns

Deeply nested code is hard to read because you have to hold the mental stack of all the outer conditions in your head while reading the inner logic.

// Deeply nested — hard to read
function processOrder(order: Order | null): string {
  if (order) {
    if (order.items.length > 0) {
      if (order.paymentVerified) {
        if (order.inStock) {
          return fulfillOrder(order);
        } else {
          return "Out of stock";
        }
      } else {
        return "Payment not verified";
      }
    } else {
      return "No items in order";
    }
  } else {
    return "Order not found";
  }
}

// Guard clauses — read top-to-bottom, happy path at the bottom
function processOrder(order: Order | null): string {
  if (!order) return "Order not found";
  if (order.items.length === 0) return "No items in order";
  if (!order.paymentVerified) return "Payment not verified";
  if (!order.inStock) return "Out of stock";

  return fulfillOrder(order);
}

The DRY Principle — and When to Break It

"Don't Repeat Yourself" is one of the most misapplied principles in software. The real insight is: duplicate knowledge, not duplicate code.

Two pieces of code might look identical but represent different concepts:

// These look the same but they might change for different reasons
function validateUserAge(age: number): boolean {
  return age >= 18;
}

function validateDrivingAge(age: number): boolean {
  return age >= 18;
}

If the legal driving age changes, you only change validateDrivingAge. If adult content rules change, you only change validateUserAge. Do not merge them just because they share a number today.

The rule: extract when the reason to change is the same, not just when the code looks the same.

Comments: Explain Why, Not What

The most useful comments explain why code does something unusual, not what it does. The "what" should be obvious from well-named code.

// Bad — the comment restates the code
// Check if user is over 18
if (user.age >= 18) { ... }

// Good — explains a non-obvious reason
// Users must be 18+ per our terms of service (legal requirement in all markets)
if (user.age >= 18) { ... }

// Good — explains a hack or workaround
// setTimeout(0) is intentional — forces this to run after the DOM paint
// to avoid a race condition with the animation library. See issue #1234.
setTimeout(() => initChart(data), 0);

The best comment is one you did not need to write because your code was clear enough.

Handle Errors Explicitly

Do not suppress errors. Do not return null when something goes wrong and let the caller figure out why. Make failures explicit and informative.

// Bad — swallowed error, caller gets null with no context
async function fetchUser(id: string): Promise<User | null> {
  try {
    return await api.get(`/users/${id}`);
  } catch {
    return null;
  }
}

// Better — let the error propagate with context
async function fetchUser(id: string): Promise<User> {
  const response = await api.get(`/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user ${id}: ${response.status} ${response.statusText}`);
  }
  return response.json();
}

Apply the Boy Scout Rule

Leave the code cleaner than you found it. Not a massive refactor — just a little better. Rename an unclear variable. Extract a method. Delete dead code.

Over time, this compounds. Codebases maintained by teams that practice this get better with age instead of rotting.

Conclusion

Clean code is not about passing a style guide. It is about making your codebase a place where problems are easy to find, changes are safe to make, and new team members can become productive quickly.

The principles are learnable. The hard part is making them habits — writing a function, noticing it does too many things, and actually taking the 10 minutes to split it. That discipline, applied consistently, is the difference between a codebase that accelerates a team and one that slows it down.

Your future self — and every engineer who works with your code — will thank you.