Readable JavaScript by Example: Ten Before-and-After Refactors
Ten small JavaScript and React refactors, from naming and parameter objects to error handling and formatting, that make code easier for the next developer to read and change.
This article is published in English.
Sources: 10 Clean Code Principles I Wish I Knew When I Started Coding
Getting a feature to work is the first half of the job. The second half is making sure a teammate, or you in six months, can read that code, change it safely and not misread it. This article works through ten concrete before-and-after refactors in JavaScript and React, each with the reasoning behind it and the point where the advice stops helping.
1. Name values after what they represent
Short, cryptic names force the reader to go and look elsewhere. In this snippet, nothing tells you what d is or what property a holds.
const d = 30;
const x = users.filter(u => u.a);
Renaming both makes the intent visible where it is used. The number becomes a trial length:
const trialPeriodDays = 30;
And the filter now reads as a sentence about active users:
const activeUsers = users.filter(user => user.isActive);
A good name says what the value stands for; generic names such as data say almost nothing.
const data = getData();
A name that states the domain concept is far more informative:
const customerOrders = getCustomerOrders();
If you feel the need to add a comment explaining a variable, that is usually a sign the name should carry that information instead.
2. Give each function one job
A function that validates input, reshapes data, writes to a database, sends email and logs activity is hard to follow and harder to test, because every test must set up all of those concerns at once. The outline below shows such a function with each responsibility inlined.
function createUser(user) {
// validate user
// format data
// save to database
// send email
// log activity
}
Splitting it into named steps turns the top-level function into a readable summary. It starts with validation:
function createUser(user) {
validateUser(user);
and continues with formatting, saving and the welcome email, each delegated to its own function:
const formattedUser = formatUser(user); saveUser(formattedUser); sendWelcomeEmail(formattedUser);
}
Now createUser() reads like a list of steps, and each helper can be tested in isolation. The trade-off is indirection: a reader who needs the details must jump into each helper, so the names must be precise enough that they rarely need to.
3. Remove duplication that shares a reason to change
Repeated logic is a candidate for extraction. Here the same tax formula appears three times.
const total1 = price1 + price1 * 0.15;
const total2 = price2 + price2 * 0.15;
const total3 = price3 + price3 * 0.15;
Pulling it into a function gives the rule a name and a single home:
function calculatePriceWithTax(price) {
return price + price * 0.15;
}
Each call site then just applies that rule:
const total1 = calculatePriceWithTax(price1);
const total2 = calculatePriceWithTax(price2);
const total3 = calculatePriceWithTax(price3);
If the rate moves from 15% to 18%, you change one line instead of hunting through the project. The important caveat is to remove meaningful duplication, not every pair of lines that happen to look alike. Two pieces of code that are identical today but change for different reasons are better left separate; merging them couples things that should evolve independently. The article on when shared code should exist explores that trade-off in depth.
4. Replace long positional parameter lists with an object
Positional arguments hide their meaning at the call site. Looking at this call, you cannot tell what 25 or true mean without opening the function definition.
createUser(
"Shayan",
"shayan@example.com",
25,
"developer",
true,
"Pakistan"
);
Passing a single object with named properties makes every value self-describing:
createUser({
name: "Shayan",
email: "shayan@example.com",
age: 25,
role: "developer",
isActive: true,
country: "Pakistan"
});
This style is already the norm in API payloads, configuration objects and React props, so it tends to feel natural in larger applications.
5. Prefer obvious code over clever code
Compressing logic into one line can make it harder to read. The line below chains optional calls over abbreviated property names.
const result = users?.filter(u => u.a)?.map(u => u.n);
Splitting it into two steps with descriptive names makes each transformation explicit. First, select the active users:
const activeUsers = users.filter(user => user.isActive);
Then, extract their names:
const activeUserNames = activeUsers.map(user => user.name);
It costs an extra line, but a reader understands it at a glance. Note one behavioural difference: the original used optional chaining, so it tolerated users being null or undefined, while the rewrite throws in that case. If users can legitimately be missing, handle that explicitly (for example with a default of []) rather than hiding it in a chain of ?.. The guiding principle is to make code obvious, not impressive.
6. Plan for failure, not only the happy path
Real systems fail: APIs return errors, users submit invalid input, networks drop, databases go offline. The following code assumes everything succeeds.
const response = await fetch("/api/users");
const users = await response.json();
A key detail is that fetch only rejects on network failures; an HTTP 404 or 500 still resolves. The improved version wraps the request in try:
try {
const response = await fetch("/api/users");
and then checks response.ok, throws on a failed status, parses the body and catches anything that went wrong:
if (!response.ok) {
throw new Error("Failed to fetch users");
} const users = await response.json(); return users;
} catch (error) {
console.error("Unable to load users:", error);
}
This makes failures visible and far easier to debug. Be aware that the catch block here logs and then implicitly returns undefined, so callers cannot tell a failure from an empty result. In real code, either rethrow after logging or return an explicit value the caller can check, so the error is handled where it can actually be dealt with.
7. Comment the why, not the what
Comments are useful; redundant ones are noise. This comment restates exactly what the line does.
// Increment counter by 1
counter++;
A good comment explains a decision the code cannot express by itself, such as why a request is deliberately delayed:
// Delay the request to avoid triggering the API rate limit.
setTimeout(fetchUsers, 1000);
A handy rule: let the code say what it does, and use comments to say why it does it.
8. Keep business rules out of JSX
In React components it is easy to pile filtering and calculation straight into the markup. This render output applies two filters inline before mapping over products.
<div>
{products
.filter(product => product.stock > 0)
.filter(product => product.price < 100)
.map(product => (
<ProductCard product={product} />
))}
</div>
Moving the rule into a named variable separates what counts as an available product from how products are displayed:
const availableProducts = products.filter(
product => product.stock > 0 && product.price < 100
);
The JSX then describes only the interface:
<div>
{availableProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
Notice that the refactored version also adds a key prop, which the original was missing and which React needs to track list items correctly.
9. Let tools enforce consistent formatting
Formatting feels minor until a codebase mixes styles. Here the same variable is declared three ways by three people: camelCase,
const userName = "Shayan";
snake_case,
const user_name = "Shayan";
and PascalCase without spacing or a semicolon:
const UserName="Shayan"
All three run, but the inconsistency adds friction every time someone reads or searches the code. Tools such as Prettier and ESLint remove the debate: Prettier formats automatically, and ESLint can enforce naming conventions and flag common mistakes.
10. Write for whoever maintains it next
This principle ties the others together. The next person to open your file might be a new colleague, a teammate or your future self, and your future self has forgotten everything you were thinking today.
Before committing, ask whether you would understand the file if you opened it cold in six months. If not, simplify: rename confusing variables, split large functions, remove duplication that shares a reason to change, and tighten the structure. The underlying goal is to reduce the amount of thinking it takes to understand the code. For more habits in the same spirit, see habits that make code easier for the next developer to change.
Wrapping up
Clean code does not mean perfect code. Deadlines, shifting requirements and evolving projects mean every team writes some messy code. What matters is the direction: each change should leave the code a little clearer, simpler and easier to maintain.
- Names should say what a value represents.
- Functions should do one job, and duplication should be removed only when it shares a reason to change.
- Objects beat long positional parameter lists.
- Readability beats brevity, but keep behaviour identical when you rewrite.
- Handle failures explicitly, and comment decisions rather than mechanics.
- Keep business rules out of markup, and let tooling own formatting.