Writing Maintainable Code
Naming, structure, comments, and the habits that make code a pleasure to come back to six months later.
Code is read far more often than it's written. The code you write today, your future self — and your teammates — will read for years. Maintainable code is code that's easy to understand, modify, and trust without re-deriving how it works.
There's no magic formula, but there are a handful of habits that move you a long way. This chapter is a tour.
Names are documentation
The single highest-leverage thing you can do is name things well. A bad name forces every reader to figure out what a thing is. A good name tells them.
A few naming rules:
- Be specific.
usersis okay;activeUsersis better. - Booleans should read like questions.
isReady,hasErrors,canEdit. - Functions should be verbs.
getUser,formatDate,validateInput. - Constants in SCREAMING_SNAKE_CASE.
- Don't abbreviate unless the abbreviation is a true standard
(
url,id,db). - Match the domain vocabulary. If users call them "orders", don't call them "purchases" in the code.
Naming is the closest thing programming has to writing prose. Good names make the comments unnecessary.
Functions should do one thing
A function that fetches data, formats it, sends an email, and updates a counter is hard to test, hard to read, and hard to change. Split it.
The smaller version is more lines, but each piece is testable, nameable, and reusable. That's almost always the right trade.
Avoid deep nesting with early returns
Pyramids of ifs are exhausting to read. Early returns —
also called guard clauses — flatten the code.
The "happy path" sits at the bottom, unindented. All the failures are out of the way. This pattern is a huge readability win.
Magic numbers and magic strings
A value that appears out of nowhere with no explanation is called magic. Give it a name.
Now changing the tax rate is a one-line edit, not a hunt through the codebase.
Comments: when and how
Comments are not a substitute for clear code, but they're invaluable in three situations:
- Why, not what. "We retry up to 3 times because the upstream API flakes ~5% of calls." The code shows what happens; the comment explains why.
- Warnings to future readers. "Don't add
awaithere — it would cause a deadlock with the lock above." - Public API documentation. A function exposed to other modules deserves a short description of its purpose, inputs, and outputs.
Bad comments to avoid:
// Increment i by 1
i++;
// Loop through users
for (const user of users) { ... }If the code says it, the comment is noise. Worse, comments lie — when the code changes, comments rot.
Consistent style
Most teams adopt a style guide and a formatter (Prettier, ESLint) that applies it automatically. The specific rules matter less than the consistency. Two strong personal habits:
- One concept per line. Don't pack multiple statements onto one line.
- Group related code. Variables used together should be declared together; imports of similar things should sit next to each other.
Handle errors meaningfully
Two anti-patterns to avoid:
Two principles:
- If you catch an error, do something with it — log, wrap, recover. Don't silently discard.
- Add context when re-throwing: which operation? which inputs? Future-you will thank present-you.
Don't comment out code — delete it
Modern version control (Git) keeps your old code safe. Leaving big blocks of commented-out code makes files harder to read and nobody trusts them anyway. Delete fearlessly.
Boundaries: trust the inside, check the outside
A useful pattern is to think of every module as having an outside (where data comes from untrusted sources) and an inside (where data has already been validated).
Validate at the boundary, then trust inside. This avoids sprinkling defensive checks throughout your logic.
The Boy Scout Rule
"Always leave the code a little better than you found it."
If you touch a file to fix a bug, rename a confusing variable along the way. If you add a feature, delete the dead code next to it. Small, opportunistic improvements compound; large rewrites rarely pay off.
Multi-file: the same code, refactored
Before:
After — same behaviour, far clearer:
Both versions return the same number. The second one is something you can come back to in six months and understand at a glance.
Challenge
Below is a function that "works" but is hard to read. Refactor it into a function called enrollStudent that:
- Returns a string starting with
"OK: "followed by the student's name when the enrollment is allowed. - Returns a string starting with
"ERROR: "followed by a short reason otherwise.
Apply at least:
- early-return / guard clauses (no deep nesting)
- meaningful names
- a named constant for the minimum age (
MIN_AGE = 16)
Rules:
- Reject if
studentis missing →"ERROR: no student" - Reject if
student.nameis missing →"ERROR: no name" - Reject if
student.age < 16→"ERROR: too young" - Reject if
student.consent !== true→"ERROR: no consent" - Otherwise →
"OK: <name>"
Which of these is the strongest sign that a function is doing too much?
It is more than 10 lines long
It uses both if and for
You can't describe its purpose in a single short sentence without using the word "and"
It uses arrow function syntax