Every codebase has that one function. You know the one. It's 200 lines long. It validates input, calls three APIs, formats dates, updates the DOM, and sends analytics. Nobody wants to touch it because changing one thing breaks something else.

I've written functions like that. I've also spent entire days debugging them. Clean functions aren't about being fancy—they're about making your future self not hate you.

The One-Thing Rule

A function should do one thing. But what counts as "one thing"?

Simple test: can you describe what the function does without using "and"? If you say "it validates the form and submits it and shows a success message," that's three things.

// ❌ Does too many things
function handleFormSubmit(formData) {
    // Validation
    if (!formData.email || !formData.email.includes('@')) {
        showError('Invalid email');
        return;
    }
    if (!formData.password || formData.password.length < 8) {
        showError('Password too short');
        return;
    }
    
    // API call
    fetch('/api/register', {
        method: 'POST',
        body: JSON.stringify(formData)
    })
    .then(res => res.json())
    .then(data => {
        // Update UI
        document.getElementById('form').style.display = 'none';
        document.getElementById('success').style.display = 'block';
        
        // Analytics
        analytics.track('registration', { userId: data.id });
    });
}
// ✅ Each function does one thing
function validateEmail(email) {
    return email && email.includes('@');
}

function validatePassword(password) {
    return password && password.length >= 8;
}

function validateForm(formData) {
    if (!validateEmail(formData.email)) {
        return { valid: false, error: 'Invalid email' };
    }
    if (!validatePassword(formData.password)) {
        return { valid: false, error: 'Password too short' };
    }
    return { valid: true };
}

async function registerUser(formData) {
    const res = await fetch('/api/register', {
        method: 'POST',
        body: JSON.stringify(formData)
    });
    return res.json();
}

function showSuccessState() {
    document.getElementById('form').style.display = 'none';
    document.getElementById('success').style.display = 'block';
}

async function handleFormSubmit(formData) {
    const validation = validateForm(formData);
    if (!validation.valid) {
        showError(validation.error);
        return;
    }
    
    const user = await registerUser(formData);
    showSuccessState();
    analytics.track('registration', { userId: user.id });
}

More lines of code? Yes. Easier to test, debug, and modify? Absolutely.

Naming Things

Good function names are like good headlines. You should know what it does without reading the body.

// ❌ What does this do?
function process(data) { }
function doStuff(x) { }
function handle(e) { }

// ✅ Self-documenting
function formatUserForDisplay(user) { }
function calculateOrderTotal(items, discount) { }
function handleLoginFormSubmit(event) { }

If you can't name the function clearly, it probably does too many things.

Avoid Side Effects

A "pure" function takes input and returns output. It doesn't modify anything outside itself. Impure functions (ones with side effects) are harder to test and reason about.

// ❌ Side effects: modifies external state
let total = 0;

function addToTotal(amount) {
    total += amount;  // Modifies global variable
}

// ✅ Pure: just calculates and returns
function calculateNewTotal(currentTotal, amount) {
    return currentTotal + amount;
}

// ❌ Side effects: modifies the input
function addTimestamp(user) {
    user.createdAt = Date.now();  // Mutates input object
    return user;
}

// ✅ Pure: returns new object
function addTimestamp(user) {
    return { ...user, createdAt: Date.now() };
}

You can't avoid all side effects—at some point you have to update the DOM or call an API. But push them to the edges. Keep your core logic pure.

Return Early

Deeply nested code is hard to follow. Return early to keep things flat.

// ❌ Nested pyramid
function processOrder(order) {
    if (order) {
        if (order.items && order.items.length > 0) {
            if (order.paymentMethod) {
                if (isValidPayment(order.paymentMethod)) {
                    // Finally, the actual logic buried 4 levels deep
                    return submitOrder(order);
                } else {
                    return { error: 'Invalid payment' };
                }
            } else {
                return { error: 'No payment method' };
            }
        } else {
            return { error: 'No items' };
        }
    } else {
        return { error: 'No order' };
    }
}

// ✅ Early returns keep it flat
function processOrder(order) {
    if (!order) {
        return { error: 'No order' };
    }
    if (!order.items?.length) {
        return { error: 'No items' };
    }
    if (!order.paymentMethod) {
        return { error: 'No payment method' };
    }
    if (!isValidPayment(order.paymentMethod)) {
        return { error: 'Invalid payment' };
    }
    
    return submitOrder(order);
}

The second version reads top to bottom. Each check is at the same indentation level. The happy path is obvious.

Limit Parameters

Functions with many parameters are hard to use and easy to mess up.

// ❌ Too many parameters
function createUser(name, email, age, country, role, isActive, createdBy) {
    // Which order was it again?
}

// ✅ Use an options object
function createUser({ name, email, age, country, role, isActive, createdBy }) {
    // Clear what each value means
}

// Usage is self-documenting
createUser({
    name: 'John',
    email: 'john@example.com',
    role: 'admin',
    isActive: true
});

As a rule of thumb: more than 3 parameters? Consider an object.

Quick Checklist

Before committing a function, ask yourself:

Clean code isn't about following rules blindly. It's about making code easier to understand, test, and change. Small functions with clear names and minimal side effects get you there.

← Back to JavaScript Articles

Back to Home