Clever code is fun to write and painful to maintain. I noticed this most clearly when I went from working alone to leading a team: the snippets I used to be proud of were exactly the ones people had to ask me about every time they needed to change something.
Here is what I aim for instead of cleverness.
Name things well
Good names remove the need for comments. If a function needs a paragraph to explain it, it usually needs a better name or a smaller scope.
// You have to read the body to know what it does
function process(d) {
return d.filter(x => x.s === 1 && x.t > Date.now());
}
// The name states the intent
function getActiveUpcomingBookings(bookings) {
return bookings.filter(
booking => booking.status === STATUS.CONFIRMED && booking.checkInAt > Date.now()
);
}
A few rules I follow:
- Functions are verbs, variables are nouns.
calculateTotal(), nottotal(). - Boolean functions start with
is,has,can.isExpired,hasPermission,canCancel. - Do not abbreviate, except for words the whole industry knows like
id,url,api. You save a few keystrokes once and make every future reader pay for it. - Name length should match the lifetime of the variable. An
iinside a three line loop is fine. Adthat lives through a 50 line function is not.
One clear signal: if a function name contains “and”, that function is doing two things.
Keep functions small and at one level of abstraction
The problem is not really line count, it is mixing levels of abstraction. The code below is 15 lines but it is tiring to read because it talks about business rules and low level details in the same breath:
async function createBooking(input) {
if (!input.roomId) throw new Error("Missing roomId");
if (!input.checkIn) throw new Error("Missing checkIn");
const conn = await pool.getConnection();
const [rows] = await conn.query(
"SELECT * FROM rooms WHERE id = ? AND deleted_at IS NULL",
[input.roomId]
);
if (!rows.length) throw new Error("Room not found");
const nights = Math.ceil((input.checkOut - input.checkIn) / 86400000);
const total = rows[0].price * nights;
// ... and so on
}
Split it up and each layer reads on its own:
async function createBooking(input) {
validateBookingInput(input);
const room = await findAvailableRoom(input.roomId, input.checkIn, input.checkOut);
const total = calculateTotal(room.price, input.checkIn, input.checkOut);
return saveBooking({ ...input, total });
}
The outer function now reads like a description of the business process. Anyone who needs the details can go into the smaller functions, and anyone who just wants the main flow reads four lines and is done.
Make the common case obvious
Optimise the reading path for the 90% case and push edge cases out to the edges.
The most useful technique here is the early return. Compare these two versions:
// The main logic is buried inside nested ifs
function cancelBooking(booking, user) {
if (booking) {
if (booking.status === "confirmed") {
if (user.id === booking.userId || user.role === "admin") {
if (booking.checkInAt > Date.now()) {
return doCancel(booking);
} else {
throw new Error("Past the cancellation deadline");
}
} else {
throw new Error("Not allowed");
}
} else {
throw new Error("Invalid status");
}
} else {
throw new Error("Not found");
}
}
// Handle every abnormal case first, main logic at the end with no indentation
function cancelBooking(booking, user) {
if (!booking) throw new Error("Not found");
if (booking.status !== "confirmed") throw new Error("Invalid status");
if (!canCancel(user, booking)) throw new Error("Not allowed");
if (booking.checkInAt <= Date.now()) throw new Error("Past the cancellation deadline");
return doCancel(booking);
}
The second version reads like a checklist, and you never have to keep track of which else branch you are in.
Comments should explain “why”, not “what”
The code already says what it does. What it cannot say is why it is like that.
// Useless comment: repeats exactly what the code says
// Increment the counter by 1
counter++;
// Valuable comment: preserves context that would otherwise be lost
// VNPAY returns response codes as strings, even for numeric values.
// Comparing with == would match "00" against 0, so === is required here.
if (response.vnp_ResponseCode === "00") {
The second kind of comment is what saves a future maintainer from “cleaning up” a line that looks redundant but is actually handling a real situation.
The best comments I have read usually start with “Do not change this to X because…”.
Consistency beats perfection
In a team, everyone writing the same way is worth more than each person writing their own optimal way.
That is why I set up ESLint and Prettier at the start of every project and run them in CI. Not because Prettier’s formatting is objectively correct, but because it ends every debate about spaces and semicolons and leaves review time for things actually worth discussing.
When you join an existing codebase, write in the style that is already there, even if you prefer something else. A file with two mixed styles is harder to read than a file consistently written in a style you dislike.
Folder structure is readable code too
New people on a project read the folder tree before they read a single line of code. Grouping by feature says more than grouping by file type:
components/ features/
Button.jsx booking/
BookingForm.jsx BookingForm.jsx
PaymentForm.jsx api.js
services/ payment/
booking.js PaymentForm.jsx
payment.js api.js
The structure on the right tells you what the app does. The one on the left only tells you what it is written with.
The reality check
The most reliable way I know to judge readability: hand the code to someone on the team who has never touched that part and ask them to explain what it does.
If they have to ask you, that is not their fault.
Readable code is a gift to your teammates, and to yourself six months from now.