Writing Cleaner JavaScript Functions: From Spaghetti to Solid Code
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.
- Functions that do things: use verbs.
getUser,validateEmail,calculateTotal - Functions that check things: use is/has/can.
isValid,hasPermission,canEdit - Event handlers:
handleClick,onSubmit,handleUserUpdate
// ❌ 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:
- Can I describe it without using "and"?
- Does the name tell me what it does?
- Does it modify things outside itself?
- Is the nesting more than 2-3 levels deep?
- Are there more than 3 parameters?
- Would I understand this in 6 months?
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.