Why clean code still matters: eight principles worth keeping
AI can refactor for you, which makes the foundations more useful rather than less. Eight practices that keep a codebase workable.
AI and the case for clean code
AI is now everywhere in this job, from generating snippets to writing whole functions, and it's reasonable to ask whether writing clean code still matters when a model can refactor it for you.
I've gone back and forth on this myself. AI is a capable assistant, but it isn't a mind reader. It can optimise, refactor and suggest clearer alternatives, and all of that works better when it starts from something sound. Give a good chef excellent ingredients and a chaotic recipe, and the dish still comes out wrong.
Clean code is about clarity, maintainability and how a project ages. It also happens to make your AI tools more effective. Here are eight principles that hold up.
1. Meaningful names
Clarity starts here. Imagine a story where every character is called "a", "b" or "c". Code is no different. Variables, functions and classes should say what they do or represent before you read a single line of their body.
let d; // What is 'd'? Days? Data?
function p(a, b) {
/* ... */
} // What does 'p' do? What are 'a' and 'b'?
Compare that with:
let daysSinceLastLogin;
function calculateTotalPrice(quantity, unitPrice) {
/* ... */
}
Good names work as documentation inside the code. They cut the mental load of reading and make debugging faster. A model reading your code benefits from the same clarity you do.
2. A function should do one thing
Every function should have one reason to change. When a function does too much, it gets harder to test, harder to follow, and more likely to break when you touch it.
function processOrder(order) {
// Validate order
// Save to database
// Send confirmation email
// Update inventory
}
Split it up instead:
function validateOrder(order) {
/* ... */
}
function saveOrder(order) {
/* ... */
}
function sendConfirmationEmail(order) {
/* ... */
}
function updateInventory(order) {
/* ... */
}
function processOrder(order) {
validateOrder(order);
saveOrder(order);
sendConfirmationEmail(order);
updateInventory(order);
}
Breaking a complex task into focused pieces makes the code modular and considerably easier to read later.
3. Don't repeat yourself
If you're writing the same logic twice, pull it into a function or a reusable component. Duplication quietly destroys maintainability: when a bug needs fixing, you have to fix it in several places, and you will eventually miss one.
// In file A
function calculateDiscountA(price) {
if (price > 100) return price * 0.9;
return price;
}
// In file B (same logic)
function calculateDiscountB(amount) {
if (amount > 100) return amount * 0.9;
return amount;
}
One implementation instead:
function applyStandardDiscount(price) {
if (price > 100) return price * 0.9;
return price;
}
// Now both A and B can use applyStandardDiscount
4. Comment why, not what
You may have heard that good code needs no comments, and that's partly true. Clear code with meaningful names makes a lot of comments redundant.
What you do need to record is why something was done a particular way, or why a workaround exists at all. That kind of comment is worth a lot.
// Increment the counter by 1
counter++;
That one adds nothing. This one does:
// This specific regex is used to handle legacy user IDs
// which sometimes contain leading zeros and special characters.
const userIdRegex = /^[0-9a-zA-Z\-_]+$/;
If a comment is needed to explain what the code does, that's usually a sign the code could be clearer.
5. Handle errors
Networks fail, users type nonsense, APIs return something unexpected. Ignoring that leads to unpredictable behaviour and users who can't tell what went wrong.
How much error handling is enough depends on context, but a reasonable rule is to handle errors where you can either recover or say something useful to the user or the system.
function getUserData(userId) {
const data = api.fetch(userId); // What if api.fetch fails?
return data.name;
}
Something closer to this:
async function getUserData(userId) {
try {
const response = await api.fetch(userId);
if (!response.ok) {
throw new Error(`Failed to fetch user data: ${response.statusText}`);
}
const data = await response.json();
return data.name;
} catch (error) {
console.error(`Error fetching user ${userId}:`, error);
// Potentially return a default value, or re-throw a custom error
throw new CustomApplicationError("User data unavailable");
}
}
6. Stay consistent
Consistency matters most on larger teams: the same naming conventions, formatting, architectural patterns and design choices across the codebase. When one part uses camelCase and another uses snake_case, reading gets harder for no reason.
Linters and formatters like ESLint and Prettier handle most of this automatically.
let firstName;
const user_id = 123;
function getProducts() {
/* ... */
}
const fetchOrders = async () => {
/* ... */
};
Versus:
let firstName;
const userId = 123;
function getProducts() {
/* ... */
}
async function fetchOrders() {
/* ... */
}
A consistent codebase reads as though one person wrote it, even when fifty people did. That cuts the overhead of moving between files.
7. Write tests
Testing can feel like a chore. It's also what lets you refactor and ship without holding your breath. With AI-assisted coding this matters more, not less: tests are how you find out whether generated code integrates correctly and hasn't broken something elsewhere.
Picture making a small change and watching an unrelated part of the application fall over. Without tests, that's a long debugging session. With tests, the failing case points at the problem.
// function to test
function add(a, b) {
return a + b;
}
// test file
describe("add function", () => {
test("should add two positive numbers correctly", () => {
expect(add(1, 2)).toBe(3);
});
test("should handle negative numbers", () => {
expect(add(-1, 5)).toBe(4);
});
test("should return zero when adding opposite numbers", () => {
expect(add(-3, 3)).toBe(0);
});
});
8. Simplicity
All of the above reduces to writing code that is simple and easy to read. Convoluted code attracts bugs and resists maintenance. Aim for the simplest solution that meets the requirement.
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.type === "premium") {
total += item.price * 1.1; // 10% premium fee
} else {
total += item.price;
}
}
return total;
}
The same thing, easier to follow:
const calculateItemPrice = (item) => {
if (item.type === "premium") {
return item.price * 1.1;
}
return item.price;
};
function calculateTotal(items) {
return items.reduce((acc, item) => acc + calculateItemPrice(item), 0);
}
Simplicity isn't about dumbing code down. It's about writing something you'll still understand in a year.
Craft doesn't go away
New tools keep arriving and AI has changed a lot, but it amplifies what you already do rather than removing the need to do it well.
Clean code is closer to a habit than a rulebook. It's consideration for your colleagues, your users and the version of you who opens this file eighteen months from now.