Clean code is the foundation of software engineering. This article explores how to write readable, maintainable high-quality code.
Why Clean Code Matters
Value of Clean Code:
┌─────────────────────────────────────────────────────┐
│ │
│ Readability │
│ └── Code is read far more than it's written │
│ │
│ Maintainability │
│ └── Fewer bugs, lower modification costs │
│ │
│ Team Collaboration │
│ └── Unified standards, less communication │
│ │
│ Long-term Benefits │
│ └── Less tech debt, faster iterations │
│ │
└─────────────────────────────────────────────────────┘
Naming Conventions
Meaningful Names
// Bad naming
const d = new Date();
const arr = users.filter(u => u.a > 18);
function calc(x: number, y: number) { return x * y; }
// Good naming
const currentDate = new Date();
const adultUsers = users.filter(user => user.age > 18);
function calculateArea(width: number, height: number) {
return width * height;
}
Naming Principles
// 1. Use searchable names
const MILLISECONDS_PER_DAY = 86400000;
const MAX_RETRY_COUNT = 3;
// 2. Use pronounceable names
// Bad: genymdhms
// Good: generationTimestamp
// 3. Use domain terminology
interface ShoppingCart {
items: CartItem[];
totalAmount: number;
discountCode?: string;
}
// 4. Be consistent
// Getting user info: pick one of getUser, fetchUser, retrieveUser and stick with it
class UserService {
getUser(id: string): User { }
getUserOrders(userId: string): Order[] { }
getUserPreferences(userId: string): Preferences { }
}
// 5. Avoid disinformation
// Bad: accountList (but it's actually a Set)
// Good: accounts or accountSet
Boolean Naming
// Boolean variables should be assertions
const isLoading = true;
const hasPermission = user.role === 'admin';
const canEdit = hasPermission && !isLocked;
const shouldShowModal = isLoggedIn && !hasSeenOnboarding;
// Functions returning booleans
function isEmpty(array: unknown[]): boolean {
return array.length === 0;
}
function hasAccess(user: User, resource: Resource): boolean {
return user.permissions.includes(resource.requiredPermission);
}
Function Design
Single Responsibility
// Bad: one function doing too much
function processUserData(user: User) {
// Validation
if (!user.email || !user.name) {
throw new Error('Invalid user');
}
// Formatting
user.name = user.name.trim().toLowerCase();
user.email = user.email.toLowerCase();
// Saving
database.save(user);
// Sending email
emailService.send(user.email, 'Welcome!');
// Logging
logger.info(`User created: ${user.id}`);
}
// Good: each function does one thing
function validateUser(user: User): void {
if (!user.email || !user.name) {
throw new Error('Invalid user data');
}
}
function normalizeUser(user: User): User {
return {
...user,
name: user.name.trim().toLowerCase(),
email: user.email.toLowerCase(),
};
}
async function createUser(user: User): Promise<User> {
validateUser(user);
const normalizedUser = normalizeUser(user);
const savedUser = await database.save(normalizedUser);
await sendWelcomeEmail(savedUser);
logger.info(`User created: ${savedUser.id}`);
return savedUser;
}
Function Arguments
// Bad: too many parameters
function createUser(
name: string,
email: string,
age: number,
address: string,
phone: string,
role: string
) { }
// Good: use object parameters
interface CreateUserParams {
name: string;
email: string;
age: number;
address?: string;
phone?: string;
role?: string;
}
function createUser(params: CreateUserParams): User {
const { name, email, age, address, phone, role = 'user' } = params;
// ...
}
// Clearer when calling
createUser({
name: 'John',
email: 'john@example.com',
age: 25,
role: 'admin',
});
Avoid Side Effects
// Bad: has side effects
let globalConfig: Config;
function initConfig() {
globalConfig = loadConfigFromFile();
globalConfig.timestamp = Date.now();
}
// Good: pure function
function loadConfig(): Config {
const config = loadConfigFromFile();
return {
...config,
timestamp: Date.now(),
};
}
// Bad: mutating input parameters
function addItem(cart: Cart, item: Item) {
cart.items.push(item);
cart.total += item.price;
}
// Good: return new object
function addItem(cart: Cart, item: Item): Cart {
return {
...cart,
items: [...cart.items, item],
total: cart.total + item.price,
};
}
Error Handling
Use Exceptions Instead of Return Codes
// Bad: return codes
function divide(a: number, b: number): number | null {
if (b === 0) return null;
return a / b;
}
const result = divide(10, 0);
if (result === null) {
console.log('Error');
}
// Good: throw exceptions
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.error('Division failed:', error.message);
}
Custom Error Classes
// Custom error types
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public code: string
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
}
}
// Usage
function getUser(id: string): User {
const user = database.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
// Handling
try {
const user = getUser('123');
} catch (error) {
if (error instanceof NotFoundError) {
return res.status(404).json({ error: error.message });
}
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
});
}
throw error;
}
Result Type
// Use Result type instead of exceptions
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function parseJson<T>(json: string): Result<T> {
try {
const data = JSON.parse(json);
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Usage
const result = parseJson<User>(jsonString);
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
Comments and Documentation
Good Comments
// Explain "why" not "what"
// Bad: increment counter
count++;
// Good: Resetting counter on page refresh may cause data loss,
// so we only initialize at session start
count++;
// Explain complex business logic
// Per banking regulations, transfers over $50,000 require additional verification
if (amount > 50000) {
requireAdditionalVerification();
}
// Mark TODOs and FIXMEs
// TODO: Add batch delete feature in next version
// FIXME: Possible race condition under high concurrency
Bad Comments
// Unnecessary comments
// This is a constructor
constructor() { }
// Add 1
i = i + 1;
// Get user
function getUser() { }
// Outdated comments are worse than no comments
// Returns username and email (actually also returns phone)
function getUserInfo() {
return { name, email, phone };
}
// Commented out code should be deleted, not kept
// function oldImplementation() {
// // old implementation
// }
Code Formatting
Vertical Formatting
// Keep related code together
class UserService {
// Public methods
async createUser(data: CreateUserDto): Promise<User> {
this.validateUserData(data);
const user = this.buildUser(data);
return this.saveUser(user);
}
async updateUser(id: string, data: UpdateUserDto): Promise<User> {
const user = await this.findUser(id);
const updated = this.mergeUserData(user, data);
return this.saveUser(updated);
}
// Private helper methods
private validateUserData(data: CreateUserDto): void {
// validation logic
}
private buildUser(data: CreateUserDto): User {
// build logic
}
private async saveUser(user: User): Promise<User> {
// save logic
}
}
Horizontal Formatting
// Use consistent indentation
function calculateTotal(items: Item[]): number {
return items.reduce((total, item) => {
const itemTotal = item.price * item.quantity;
const discount = item.discount || 0;
return total + itemTotal - discount;
}, 0);
}
// Reasonable line length (80-120 characters)
const result = await userService
.findByEmail(email)
.then(user => user.profile)
.then(profile => profile.settings);
Code Refactoring
Extract Function
// Before refactoring
function printOwing(invoice: Invoice) {
let outstanding = 0;
// Calculate outstanding
for (const order of invoice.orders) {
outstanding += order.amount;
}
// Print banner
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
// Print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
// After refactoring
function printOwing(invoice: Invoice) {
const outstanding = calculateOutstanding(invoice);
printBanner();
printDetails(invoice.customer, outstanding);
}
function calculateOutstanding(invoice: Invoice): number {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function printBanner(): void {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function printDetails(customer: string, amount: number): void {
console.log(`name: ${customer}`);
console.log(`amount: ${amount}`);
}
Early Return
// Before refactoring
function getPayAmount(employee: Employee): number {
let result: number;
if (employee.isSeparated) {
result = getSeparatedAmount();
} else {
if (employee.isRetired) {
result = getRetiredAmount();
} else {
result = getNormalAmount();
}
}
return result;
}
// After refactoring
function getPayAmount(employee: Employee): number {
if (employee.isSeparated) {
return getSeparatedAmount();
}
if (employee.isRetired) {
return getRetiredAmount();
}
return getNormalAmount();
}
Best Practices Summary
Clean Code Principles:
┌─────────────────────────────────────────────────────┐
│ Naming │
│ ├── Reveal intention │
│ ├── Avoid encodings and prefixes │
│ ├── Use searchable names │
│ └── Be consistent │
│ │
│ Functions │
│ ├── Keep them small │
│ ├── Single responsibility │
│ ├── Few arguments │
│ └── No side effects │
│ │
│ Error Handling │
│ ├── Use exceptions │
│ ├── Provide context │
│ ├── Define exception types │
│ └── Don't return null │
│ │
│ Refactoring │
│ ├── Refactor continuously │
│ ├── Take small steps │
│ ├── Tests as safety net │
│ └── Eliminate duplication │
└─────────────────────────────────────────────────────┘
Code is written for humans to read, and only incidentally for machines to execute. Clean code shows respect for your team.