How to Write Clean JavaScript Code

How to Write Clean JavaScript Code

You have written code that works. It passes the tests, the feature ships, and nobody complains. Six months later you open the file again — and you cannot tell what half of it does. That is the problem clean code solves.

Clean code is not about style preferences or lint rules for their own sake. It is about making the next change easy. The code you write today will be read far more often than it is written, usually by you, usually when you have forgotten everything about it.

This guide covers the habits that actually matter in JavaScript: small functions, meaningful names, fewer side effects, predictable error handling, safe async patterns, sensible use of immutability, and the tooling that keeps a codebase consistent without anyone having to argue about it.

In one sentence: Clean code is code that does one thing per function, names things by what they mean, keeps side effects where you can see them, and handles errors in a way that the next reader can follow.

Why Clean Code Matters

The case for clean code is not aesthetic. It is about the cost of change. Every hour spent deciphering a function is an hour not spent fixing a bug or building a feature, and that cost compounds as a codebase grows.

Unclean

What it costs

Every change requires reading three files to understand one function.

Bugs hide because nobody can see the whole flow in one place.

New developers take weeks to become productive.

Refactoring feels too risky, so the mess grows.

Clean

What it buys

A function name tells you what it does before you open it.

Changing one thing only affects one place.

Tests are easy to write because functions are small and predictable.

Refactoring is safe because the code is already organized.

A useful way to measure it

Time how long it takes to make a small change in an unfamiliar part of a codebase. If the answer is “five minutes,” the code is probably clean. If the answer is “I need to ask someone,” it is not.

The Core Principles

Everything else in this guide is a consequence of these seven ideas. They are not new — most of them come from a body of writing on software design that predates JavaScript entirely — but they apply directly to how you write a function or name a variable today.

01

One thing per function

A function with a single reason to change is easy to name, test, and reuse.

02

Names carry meaning

A name should say what a value is or what a function does. Nothing more, nothing less.

03

Prefer pure functions

Read the inputs, return a result, change nothing outside. The easiest kind of code to reason about.

04

Keep mutation local

Mutating a local variable is fine. Mutating a shared one is where bugs live.

05

Fail loudly, handle deliberately

Throw for exceptional cases and catch only what you can actually do something about.

06

Return early, nest rarely

Handle the exceptional cases first, then continue at the top level. The happy path stays readable.

07

Consistency beats preference

Any consistent convention is easier to read than three good ones mixed together.

Small Functions That Do One Thing

The single most effective change you can make to any codebase is to shorten its functions. A function that does one thing has a name you can guess, arguments you can understand, and a body you can hold in your head.

How to tell a function is doing too much

  • You need the word and to describe what it does.
  • It has more than about three parameters.
  • It needs a comment to explain its sections.
  • Parts of it could be reused somewhere else, but cannot be because they are buried inside.
  • You cannot test one behaviour without setting up all the others.

Before and after

Incorrect — validation, transformation, and saving in one function
function processOrder(order) { if (!order) throw new Error('no order'); if (!order.items || order.items.length === 0) { throw new Error('no items'); } if (!order.customer) throw new Error('no customer'); let total = 0; for (let i = 0; i < order.items.length; i++) { const item = order.items[i]; if (item.quantity < 0) throw new Error('bad qty'); total += item.price * item.quantity; } total = total * 1.2; // tax order.total = total; order.status = 'processed'; saveToDatabase(order); sendEmail(order.customer.email, 'Order confirmed'); return order; } // Four jobs in one function. // Cannot reuse the total calculation. // Cannot test saving without sending email.
Correct — each function does one thing
function validateOrder(order) { if (!order) throw new Error('no order'); if (!order.items?.length) throw new Error('no items'); if (!order.customer) throw new Error('no customer'); } function calculateSubtotal(items) { return items.reduce((sum, item) => { if (item.quantity < 0) throw new Error('bad qty'); return sum + item.price * item.quantity; }, 0); } function calculateTotal(subtotal, taxRate = 0.2) { return subtotal * (1 + taxRate); } async function processOrder(order) { validateOrder(order); const subtotal = calculateSubtotal(order.items); order.total = calculateTotal(subtotal); order.status = 'processed'; await saveToDatabase(order); await sendConfirmation(order.customer.email); return order; } // Each piece is named, testable, // and reusable on its own.

Extract until it is obvious

The test for a well-extracted function is that reading its body tells you what it does without any further explanation. If you can read the function aloud as a sentence, the extraction was right.

// Reads like a sentence:
// "An order is valid if it has items and a customer"
function isValidOrder(order) {
    return Boolean(order?.items?.length && order?.customer);
}

// "An item subtotal is price times quantity"
function itemSubtotal(item) {
    return item.price * item.quantity;
}

// "The order subtotal is the sum of every item subtotal"
function orderSubtotal(items) {
    return items.reduce((sum, item) => sum + itemSubtotal(item), 0);
}

// "A total is the subtotal plus tax"
function applyTax(subtotal, rate) {
    return subtotal * (1 + rate);
}

// And then the top-level function just reads well:
function calculateOrderTotal(order) {
    if (!isValidOrder(order)) return 0;
    return applyTax(orderSubtotal(order.items), TAX_RATE);
}
Extract until it hurts, then stop

It is possible to go too far — a function that returns a + b is not worth extracting. The rule of thumb: extract when the extracted code has a name worth using more than once, or when the surrounding function becomes clearer as a result.

Names That Say What They Mean

A name is the smallest piece of documentation in a program. The conventions are covered in JavaScript Naming Conventions; this section is about the choice of which name, not the casing.

Name by meaning, not by type or content

Incorrect — describes the container
const data = getUser(); const list = user.posts; const str = user.name; const arr2 = filtered; const temp = calculateTotal(cart); const obj = { a: 1 }; // Every name describes how the value // is shaped or where it came from, // not what it means.
Correct — describes the meaning
const currentUser = getUser(); const userPosts = user.posts; const userName = user.name; const activeUsers = filtered; const cartTotal = calculateTotal(cart); const searchOptions = { a: 1 }; // Each name says what the value // represents.

Verbs for functions, nouns for values

// A function name should describe the action
function calculateTotal(items) { }
function fetchUser(id) { }
function renderCard(data) { }
function isValidEmail(value) { }
function formatPrice(cents) { }

// A variable name should describe the value
const currentUser = fetchUser(id);
const cartTotal = calculateTotal(items);
const emailIsValid = isValidEmail(input);

// Nouns as function names are a smell —
// they usually mean the function is doing
// too much or the name is not specific
function user(id) { }        // what about the user?
function total(items) { }    // total what?

// Booleans should read as a question
const isLoading = true;
const hasPermission = false;
const canEdit = true;
const shouldRetry = false;

Avoid catch-all names

data, info, temp, result, and item are placeholders masquerading as names. They tell you nothing and they make every line that uses them harder to read.

// Vague — the reader has to look elsewhere
function process(data) {
    const info = data.details;
    const temp = info.value * 2;
    const result = temp + 10;
    return result;
}

// Specific — the names carry the meaning
function calculateDiscountedPrice(product) {
    const discountDetails = product.discount;
    const doubledDiscount = discountDetails.value * 2;
    const finalPrice = doubledDiscount + BASE_FEE;
    return finalPrice;
}
A name that lies is worse than one that is vague

data makes you go read the code. A variable called itemCount that actually holds a string makes you trust something false. When the value changes, rename the variable.

Fewer Side Effects, Clearer Code

A pure function reads only its arguments and returns a value. It does not modify anything outside itself, does not log, does not fetch, and does not set a global. It is the easiest kind of code to test, reuse, and reason about.

function applyTax(subtotal) { Reads only its parameter
return subtotal * 1.2; Returns a new value
} Changes nothing outside

The same function with a side effect is harder to trust. It looks the same at the call site, but it does something extra.

Incorrect — hidden side effects
let lastTotal = 0; function applyTax(subtotal) { lastTotal = subtotal * 1.2; // modifies outer state console.log('tax applied'); // logs trackEvent('tax'); // network call return lastTotal; } // Same signature, very different // behaviour. Cannot test in isolation.
Correct — pure, with side effects moved out
function applyTax(subtotal, rate) { return subtotal * (1 + rate); } // The caller decides what to do // with the result and when to // log or track: const total = applyTax(subtotal, TAX_RATE); logTaxApplied(total); trackEvent('tax');

Mutating a local variable is fine

Purism about mutation misses the point. A local accumulator inside a function is completely fine. The problem is mutating something shared — a parameter, a module-level object, or a value owned by another part of the program.

Incorrect — mutates a parameter
function addTax(order) { order.tax = order.subtotal * 0.2; order.total = order.subtotal + order.tax; return order; } // The caller's object is now // different. Anyone holding a // reference sees the change, // whether they expected it or not.
Correct — returns a new object
function addTax(order) { const tax = order.subtotal * 0.2; return { ...order, tax, total: order.subtotal + tax }; } // The caller's object is untouched. // A new one is returned.

Watch for accidental mutation through references

// This looks harmless but mutates the original
function addItem(cart, item) {
    cart.items.push(item);   // mutates the caller's array
    return cart;
}

// Two fixes, depending on intent

// 1. Return a new object (preferred for shared state)
function addItemImmutable(cart, item) {
    return {
        ...cart,
        items: [...cart.items, item]
    };
}

// 2. Or make the mutation explicit in the name
function addItemInPlace(cart, item) {
    cart.items.push(item);
    return cart;
}

// The second is fine if the mutating
// behaviour is the point — the name
// tells the caller what to expect.
Sort is the classic trap

Array.prototype.sort mutates the array it is called on. [...items].sort() is the safe way to sort without touching the original.

Control Flow: Early Return and Fewer Branches

Deeply nested code is hard to read because you have to hold every surrounding condition in your head while you read the innermost line. Early return flattens that.

Incorrect — nested conditions
function getDiscount(user) { if (user) { if (user.isActive) { if (user.membership) { if (user.membership === 'gold') { return 0.2; } else if (user.membership === 'silver') { return 0.1; } else { return 0; } } else { return 0; } } else { return 0; } } else { return 0; } } // Four levels of nesting to find // a number.
Correct — early return
function getDiscount(user) { if (!user?.isActive) return 0; if (!user.membership) return 0; const rates = { gold: 0.2, silver: 0.1 }; return rates[user.membership] ?? 0; } // The exceptional cases are handled // first, and the happy path is at // the top level.

Prefer a lookup table to a long if/else chain

// Instead of a chain of conditions
function getStatusLabel(status) {
    if (status === 'pending') return 'Waiting';
    if (status === 'shipped') return 'On the way';
    if (status === 'delivered') return 'Arrived';
    if (status === 'cancelled') return 'Cancelled';
    return 'Unknown';
}

// Use a map
const STATUS_LABELS = {
    pending: 'Waiting',
    shipped: 'On the way',
    delivered: 'Arrived',
    cancelled: 'Cancelled'
};

function getStatusLabel(status) {
    return STATUS_LABELS[status] ?? 'Unknown';
}

// Adding a status is one line
// in the map, not a new branch.

Avoid flag arguments

A boolean parameter that switches what a function does is a sign the function has two behaviours. Split it into two functions with names that describe each case.

Incorrect — a flag parameter
function render(user, isAdmin) { if (isAdmin) { // render with admin controls } else { // render without } } render(user, true); // what does true mean? render(user, false);
Correct — two named functions
function renderAdminView(user) { } function renderUserView(user) { } renderAdminView(user); renderUserView(user); // No boolean to decode at // the call site.

Error Handling That Actually Helps

Errors are information. Handling them well means the reader can tell what can go wrong, what happened when it did, and what the program is going to do about it.

Throw for exceptional cases, catch only what you can handle

Incorrect — swallowing the error
try { const data = JSON.parse(input); } catch (e) { // silently ignore } // The failure is invisible. The // variable `data` is now undefined // and the bug surfaces later.
Correct — handle what you can, propagate the rest
try { const data = JSON.parse(input); } catch (error) { if (error instanceof SyntaxError) { showMessage('Invalid JSON'); return null; } throw error; // something else — let it propagate }

Write meaningful error messages

// Useless — says nothing about what happened
throw new Error('Error');

// Better — says what went wrong
throw new Error('Invalid JSON input');

// Best — says what went wrong and where
function parseUser(json) {
    try {
        return JSON.parse(json);
    } catch (error) {
        throw new Error(
            `Failed to parse user JSON: ${error.message}`,
            { cause: error }
        );
    }
}

// Custom error classes let callers handle
// specific failure types
class ValidationError extends Error {
    constructor(field, message) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
    }
}

// And callers can distinguish them
try {
    createUser(input);
} catch (error) {
    if (error instanceof ValidationError) {
        highlightField(error.field);
    } else {
        throw error;
    }
}

Do not use errors for control flow

Incorrect — errors as branching
function findUser(id) { const user = users[id]; if (!user) throw new Error('not found'); return user; } try { const user = findUser(id); doSomething(user); } catch { // "not found" is a normal case, // not an exceptional one doSomethingElse(); }
Correct — return a value for the normal case
function findUser(id) { return users[id] ?? null; } const user = findUser(id); if (user) { doSomething(user); } else { doSomethingElse(); } // Errors are for the exceptional. // Absence of a value is normal.
An empty catch is a bug factory

try { } catch (e) { } hides everything, including the errors you did not anticipate. If you genuinely want to ignore a failure, write a comment explaining why — it will save someone an afternoon.

Clean Asynchronous Code

Asynchronous code has one main trap: doing things in sequence that could happen in parallel, and doing them in a way that makes error handling hard to follow. Both have simple fixes.

Prefer async/await to long promise chains

Incorrect — a long chain
function loadDashboard(userId) { return fetchUser(userId) .then(user => fetchPosts(user.id)) .then(posts => fetchComments(posts[0].id)) .then(comments => { return { user, posts, comments }; }) .catch(error => { console.error(error); throw error; }); } // The `user` variable is out of // scope in the final .then, and // the flow is hard to follow.
Correct — async/await
async function loadDashboard(userId) { try { const user = await fetchUser(userId); const posts = await fetchPosts(user.id); const comments = await fetchComments(posts[0].id); return { user, posts, comments }; } catch (error) { logError(error); throw error; } } // Reads sequentially. Every value // stays in scope. Errors are // handled in one place.

Run independent requests in parallel

Two awaits in sequence take the sum of both durations. Promise.all takes the longer of the two.

Incorrect — sequential, twice the wait
async function loadUserPage(id) { const user = await fetchUser(id); // 200ms const posts = await fetchPosts(id); // 300ms const stats = await fetchStats(id); // 150ms return { user, posts, stats }; // Total: about 650ms, because // each one waits for the one // before it. }
Correct — parallel
async function loadUserPage(id) { const [user, posts, stats] = await Promise.all([ fetchUser(id), fetchPosts(id), fetchStats(id) ]); return { user, posts, stats }; // Total: about 300ms — the // longest of the three, because // they all started at once. }

Do not await inside a loop when you do not have to

// Sequential — each iteration waits for the one before
async function fetchAllUsers(ids) {
    const users = [];
    for (const id of ids) {
        users.push(await fetchUser(id));   // slow
    }
    return users;
}

// Parallel — all requests fire at once
async function fetchAllUsers(ids) {
    return Promise.all(ids.map(id => fetchUser(id)));
}

// If the loop genuinely must be sequential
// — for example, because each request depends
// on the previous response — keep the await
// inside, but say so in a comment.
Promise.allSettled for partial failure

Promise.all rejects as soon as one request fails. Promise.allSettled waits for all of them and gives you a status for each, which is usually what you want when rendering a dashboard of independent panels.

Structure and Organization

Clean code is not only about individual functions. It is also about where things live and how the pieces relate to each other.

One responsibility per file

  • A file should have one reason to change. If the user model and the user API client live together, they will change for unrelated reasons.
  • Group by feature, not by type. A checkout/ folder holding the component, the API call, and the types is easier to navigate than three folders that each hold one piece.
  • Keep the public surface small. Export what other modules need, and keep the rest internal.
  • Name files after what they export. userService.js exports a user service.

Keep related code together

Incorrect — scattered
// constants.js export const TAX_RATE = 0.2; // utils.js export function applyTax(n) { return n * 1.2; // hard-coded } // cart.js const total = applyTax(subtotal); // The tax rate exists in two // places and one of them is // already out of sync.
Correct — one source of truth
// cart.js const TAX_RATE = 0.2; function applyTax(subtotal) { return subtotal * (1 + TAX_RATE); } // Constants and the functions // that use them live together, // so they cannot drift apart.

Module boundaries

// userService.js — one responsibility

const API_URL = '/api/users';

async function findById(id) {
    const response = await fetch(`${API_URL}/${id}`);
    if (!response.ok) throw new Error(`User ${id} not found`);
    return response.json();
}

async function create(data) {
    const response = await fetch(API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error('Could not create user');
    return response.json();
}

export { findById, create };

// The module exposes two named
// functions and nothing else.
// Everything else is internal.
Prefer named exports to default exports

A named export makes it obvious what is being imported, and a typo becomes an error instead of undefined. Default exports are convenient for a single-export module, but they hide the name from the importer.

A Full Refactor, Step by Step

The best way to see what clean code looks like is to take a messy function and clean it up. Here is one, with each step explained.

The original

function p(d) {
    if (d && d.u && d.i && d.i.length > 0) {
        let t = 0;
        for (let i = 0; i < d.i.length; i++) {
            const x = d.i[i];
            if (x.q > 0) {
                t = t + x.pr * x.q;
            }
        }
        t = t * 1.2;
        d.t = t;
        save(d);
        return d;
    } else {
        return null;
    }
}

// What is `d`? What is `p`?
// What is `t`, `i`, `x`, `q`, `pr`?
// The reader has to open the
// implementation and work it out.

The refactor, step by step

1
Name the function and the parameter p becomes processOrder, and d becomes order. The function now says what it does.
2
Replace early-exit conditions with early returns The wrapping if / else becomes a guard clause at the top, so the happy path is at the top level.
3
Extract the total calculation The loop becomes calculateSubtotal(items), which can be tested on its own.
4
Extract the tax rate The magic 1.2 becomes a named constant with a comment explaining what it represents.
5
Return a new object instead of mutating Instead of setting d.t and d.status, return a new object with the computed values.

After the refactor

const TAX_RATE = 0.2;   // 20% VAT, fixed by legislation

function itemSubtotal(item) {
    return item.price * item.quantity;
}

function calculateSubtotal(items) {
    return items
        .filter(item => item.quantity > 0)
        .reduce((sum, item) => sum + itemSubtotal(item), 0);
}

function calculateTotal(subtotal, rate = TAX_RATE) {
    return subtotal * (1 + rate);
}

async function processOrder(order) {
    if (!order?.items?.length) {
        return null;
    }

    const subtotal = calculateSubtotal(order.items);

    const processed = {
        ...order,
        total: calculateTotal(subtotal),
        status: 'processed'
    };

    await save(processed);

    return processed;
}

// Every name says what it holds.
// Every function does one thing.
// The caller's order is untouched.
// Each piece is testable in isolation.

What the refactor changed

Every improvement in the refactor, and why it matters
Before After Why
p(d) processOrder(order) The reader knows what it does from the call site
One nested if / else A guard clause at the top The happy path is not indented inside a condition
Inline for loop calculateSubtotal(items) Testable on its own; reusable elsewhere
Magic number 1.2 TAX_RATE with a comment The number has a name and a reason
d.t = t (mutation) A new object returned The caller’s object is untouched
Single-letter names Descriptive names No comment needed to explain them

Tooling That Keeps It Clean

A convention that lives only in a document drifts. A convention enforced by a tool holds. The setup takes an afternoon and pays back forever.

Prettier for formatting

Prettier removes every argument about whitespace, quotes, and line breaks. It formats on save, so nobody has to think about it.

// .prettierrc

{
    "semi": true,
    "singleQuote": true,
    "tabWidth": 4,
    "trailingComma": "es5",
    "printWidth": 90,
    "arrowParens": "avoid"
}

// Any of these choices is fine. The point is
// that everyone on the team uses the same one.

ESLint for correctness

// eslint.config.js — the rules that catch real mistakes

export default [
    {
        rules: {
            // Variables
            'no-unused-vars': 'error',
            'no-shadow': 'error',
            'no-redeclare': 'error',
            'prefer-const': 'error',

            // Functions
            'no-param-reassign': 'error',
            'max-params': ['warn', 4],
            'max-depth': ['warn', 4],
            'max-lines-per-function': ['warn', 60],

            // Async
            'no-async-promise-executor': 'error',
            'require-await': 'warn',

            // Comparison and equality
            eqeqeq: ['error', 'always'],
            'no-implicit-coercion': 'warn',

            // Errors
            'no-empty': ['error', { allowEmptyCatch: false }]
        }
    }
];

What each rule buys you

The rules worth turning on for any project
Rule Catches
no-unused-vars Names declared and never read — often a sign of a typo elsewhere
no-shadow Inner variables hiding outer ones with the same name
no-param-reassign The accidental mutation of function arguments
prefer-const let used where the value never changes
eqeqeq == where === was meant
no-empty Empty catch blocks that hide errors
max-depth Deep nesting that should be early returns
max-params Functions taking too many arguments — a sign they do too much

Run it in CI

// package.json

{
    "scripts": {
        "lint":       "eslint .",
        "format":     "prettier --write .",
        "format:check": "prettier --check .",
        "test":       "vitest run",
        "ci":         "npm run format:check && npm run lint && npm run test"
    }
}

// A linter that only runs when someone
// remembers is a linter that does not run.
// Add it to the pipeline.
Turn on the rules you can fix today

Do not enable every rule at once and face two thousand errors. Turn on the ones you can fix now, fix them, and add the rest over time. Incremental adoption is the only kind that sticks.

Anti-Patterns to Avoid

These are the patterns that look reasonable in isolation and become problems as a codebase grows. Recognizing them early saves a lot of rework.

1. The god function

A single function that handles validation, business logic, persistence, and side effects. Split it into named pieces — each one testable on its own.

2. Boolean trap parameters

Incorrect — what does true mean?
createUser('Ada', true, false); render(user, true); fetchData(id, false, true);
Correct — options object or named functions
createUser('Ada', { isAdmin: true, sendEmail: false }); renderAdminView(user); fetchData(id, { cache: false });

3. Magic numbers and strings

Incorrect — unexplained literals
if (status === 3) { } if (user.role === 'a') { } setTimeout(fn, 86400000); // What is 3? What is 'a'? // Why a full day in milliseconds?
Correct — named values
const STATUS_ARCHIVED = 3; const ROLE_ADMIN = 'a'; const ONE_DAY_MS = 24 * 60 * 60 * 1000; if (status === STATUS_ARCHIVED) { } if (user.role === ROLE_ADMIN) { } setTimeout(fn, ONE_DAY_MS);

4. Comments that explain the code instead of the reason

Incorrect — the comment restates the code
// Set the timeout to 5000 const timeout = 5000; // Loop over the items items.forEach(item => { }); // Check if the user is logged in if (user) { }
Correct — explains a decision
// 5s matches the load balancer's idle timeout — // anything longer gets cut off at the proxy const timeout = 5000; // The API returns items in reverse order, // so the array is reversed before rendering items.reverse();

5. Deeply nested callbacks and promise chains

Use async/await with try/catch. It reads sequentially and keeps errors in one place.

6. Mixing mutation and return values

Incorrect — does both, says neither
function sortUsers(users) { users.sort((a, b) => a.name.localeCompare(b.name)); return users; } // The caller cannot tell whether // their array was modified.
Correct — pick one, say so in the name
// Returns a new array function sortedUsers(users) { return [...users].sort((a, b) => a.name.localeCompare(b.name)); } // Or mutates deliberately function sortUsersInPlace(users) { users.sort((a, b) => a.name.localeCompare(b.name)); }

7. Catch-all try blocks

A try block wrapping fifty lines hides which line actually failed. Keep try blocks small and specific.

8. Global mutable state

Incorrect — anyone can change it
let currentUser = null; function setUser(user) { currentUser = user; } function render() { // relies on currentUser being set } // The order in which these are called // matters, and nothing enforces it.
Correct — pass it explicitly
function render(user) { // the caller decides which // user to render } render(currentUser); // The dependency is visible in // the function signature.

A Practical Checklist

Before you consider a function finished, run through this list. Most items take seconds to check and save minutes later.

  • Can the function name be read aloud as a sentence describing what it does?
  • Does it do one thing — no and in the description?
  • Are all names meaningful, with no data, temp, or single letters outside a tiny scope?
  • Are booleans prefixed with is, has, can, or should?
  • Does the function avoid mutating its parameters?
  • Are the exceptional cases handled first, with the happy path at the top level?
  • Is every catch block either handling the error or re-throwing it?
  • Are independent async operations run in parallel with Promise.all?
  • Are magic numbers and strings named as constants?
  • Do comments explain the why, not the what?
  • Does the file have one reason to change?
  • Does the whole thing pass the linter and the formatter without complaints?

Frequently Asked Questions

1. What does clean code mean in JavaScript?

Clean code is code that is easy to read, easy to change, and hard to break. It uses small focused functions, meaningful names, predictable error handling, and consistent structure, so the next developer can understand and modify it without guessing.

2. How long should a JavaScript function be?

There is no hard rule, but a function that fits on one screen — roughly 20 to 30 lines — is usually doing one thing. If a function needs a comment to explain its sections, it is probably two functions.

3. What does “a function should do one thing” mean?

It means a function should have a single reason to change. If you can describe what it does without using the word and, it is probably doing one thing. Functions that validate, transform, and save are three functions, not one.

4. Should I use var, let, or const?

Use const by default, let only when the value must be reassigned, and avoid var entirely. This makes it obvious at a glance which values are fixed and which change, and it avoids var‘s confusing scoping and hoisting behavior.

5. Why should I avoid side effects in functions?

A function that modifies something outside itself is harder to test, harder to reuse, and harder to reason about. When a function only reads its inputs and returns a result, you can call it anywhere without worrying about what else it changed.

6. Is mutation always bad in JavaScript?

No. Mutating a local variable inside a function is completely fine. The problem is mutating something shared — a function parameter, a module-level object, or a value owned by another part of the program. Keep mutation local.

7. How should I handle errors in JavaScript?

Throw for exceptional cases, catch only what you can handle, and let everything else propagate. Avoid empty catch blocks that hide bugs, and prefer async/await with try/catch over long promise chains for readability.

8. When should I write a comment?

Comment the why, not the what. The code already says what it does, so reserve comments for decisions that are not obvious — a workaround, a constraint, a non-obvious choice of value, or a note explaining why an apparently wrong approach is correct.

9. What is the difference between early return and deep nesting?

Early return handles the exceptional cases first and then continues at the top level. Deep nesting indents every condition inside the previous one. Early return keeps the happy path readable and is usually the better choice.

10. Should I use a linter and formatter?

Yes. A formatter such as Prettier removes every argument about whitespace, quotes, and line breaks, and a linter such as ESLint catches real mistakes — unused variables, shadowed names, missing returns. Together they keep a codebase consistent without effort.

11. How do I deal with deeply nested async code?

Use async/await with try/catch. It reads sequentially, handles errors in one place, and does not need nested callbacks. Run independent requests in parallel with Promise.all instead of awaiting them one at a time.

12. Does clean code matter for a small project?

Yes, and it costs almost nothing. A small project that is written clearly is still easy to extend when it grows. The habits that make a large codebase maintainable — small functions, clear names, predictable errors — are the same habits you use on the first day.

Key Takeaways

  • Clean code is about making the next change easy — not about style for its own sake.
  • A function should do one thing; if you need and to describe it, split it.
  • Name by meaning, not by type or content. Booleans read as a question.
  • Prefer pure functions — read the inputs, return a result, change nothing outside.
  • Mutating a local variable is fine; mutating a shared one is where bugs live.
  • Use early returns so the happy path stays at the top level.
  • Replace long condition chains with a lookup table.
  • Avoid boolean flag parameters — split into two named functions.
  • Catch only what you can handle, and never leave an empty catch block.
  • Prefer async/await to long promise chains.
  • Run independent async operations in parallel with Promise.all.
  • Use Prettier for formatting and ESLint for correctness, and run both in CI.

What to Learn Next

Now that the habits are clear, the next step is the language details they rest on. Read JavaScript Naming Conventions for the casing rules, and JavaScript Comments for the documentation side.

From there, explore JavaScript Functions Explained, JavaScript Array Methods, and JavaScript Statements and Expressions. For the async side, read Async and Await Explained and How JavaScript Works.

If you are still connecting the pieces, read What Is JavaScript?, JavaScript Strict Mode, and JavaScript vs TypeScript for the surrounding context.

Practice Challenge

The fastest way to learn clean code is to clean up something dirty. This exercise takes about thirty minutes.

  1. Create index.html and app.js, and load the script with <script src="app.js" defer></script>.
  2. Write a deliberately messy function: a one-letter name, a single-letter parameter, an inline loop, a magic number, and a try block that catches and ignores errors.
  3. Add a global mutable variable that the function reads, such as let currentUser.
  4. Have the function both mutate its parameter and return it, so the caller cannot tell which it does.
  5. Have the function do three things — validate, transform, and save — all in one body, with the save wrapped in try/catch.
  6. Confirm the script runs. Then read it out loud. Note every place you have to stop and work out what a name means.
  7. Now refactor step by step. Rename the function to a verb phrase and the parameter to a descriptive noun.
  8. Extract the loop into a named function that does one thing.
  9. Replace the magic number with a named constant, with a comment explaining where the value comes from.
  10. Replace the nested condition with a guard clause at the top.
  11. Change the function to return a new object instead of mutating its parameter.
  12. Move the save call out of the try/catch and handle the error explicitly — re-throw anything you cannot handle.
  13. Remove the global variable and pass the value as a parameter instead.
  14. Finally, install Prettier and ESLint. Enable no-unused-vars, no-param-reassign, prefer-const, and eqeqeq. Run both and fix anything they flag.

If you complete those steps, you have written code that breaks nearly every rule in this guide, refactored it into something readable, moved a side effect out of a hidden place, and configured the tooling that keeps the rules in place. That last part matters most: the refactor teaches you what clean code looks like, and the linter makes sure the next person writes it that way too.

Inar Learn
Inar Learnhttps://inarlearn.com
Inar Learn is an innovative online learning platform offering high-quality courses, tutorials, and resources to help learners gain practical skills and grow their knowledge.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here