Posted in

How to Write Clean Code as a Junior Developer

How to Write Clean Code as a Junior Developer
How to Write Clean Code as a Junior Developer

“Clean code” is advice that sounds obvious and is genuinely hard to apply. Most juniors have read the rules and still produce code that reviewers mark up heavily. The gap is usually not knowledge of the rules but knowing which ones matter in a given situation. This guide focuses on the habits that produce the largest improvement per unit of effort in your first few years.

The standard that actually matters

Code is read far more often than it is written, and the reader is frequently you, six months later, with no memory of the context. Every rule below reduces to one question: will the next person understand this without asking me?

That framing is more useful than a checklist because it settles arguments. A clever one-liner that saves three lines but requires two minutes of study fails the test. A slightly longer function that reads top to bottom passes it.

Naming carries more weight than anything else

The fastest improvement available to most juniors is better names.

// Before
const d = new Date();
const x = u.filter(i => i.a > 18);
function proc(data) { }

// After
const currentDate = new Date();
const adultUsers = users.filter(user => user.age > 18);
function calculateShippingCost(order) { }

Some practical rules. Functions should be verb phrases, because they do something. Booleans should read as questions: isActive, hasPermission, canEdit. Arrays should be plural. Avoid abbreviations unless they are universal in your domain — id and url are fine, usrMgr is not.

The single-letter exception is genuinely narrow: i in a short loop, x and y for coordinates, e for an event handler parameter. Everywhere else, the characters you save cost the reader more than they save you.

A name should also not lie. A function called getUser that also writes to a cache and sends an analytics event is misnamed, and the misnaming will eventually cause a bug when someone calls it expecting a read.

One function, one job

A function that fetches, validates, transforms, saves and emails is five functions wearing a coat. It cannot be tested in parts, reused in parts or understood in parts.

// Doing too much
async function handleSignup(req) {
  const { email, password } = req.body;
  if (!email.includes("@")) throw new Error("Invalid email");
  if (password.length < 8) throw new Error("Password too short");
  const hashed = await bcrypt.hash(password, 10);
  const user = await db.users.insert({ email, password: hashed });
  await sendWelcomeEmail(user.email);
  return user;
}
// Split by responsibility
function validateSignup({ email, password }) { /* ... */ }

async function createUser({ email, password }) {
  const hashed = await bcrypt.hash(password, 10);
  return db.users.insert({ email, password: hashed });
}

async function handleSignup(req) {
  validateSignup(req.body);
  const user = await createUser(req.body);
  await sendWelcomeEmail(user.email);
  return user;
}

The rewritten handler reads like a description of the process. Each piece can be tested alone, and changing the hashing strategy touches one function.

Line-count rules like “no function over twenty lines” are a crude proxy for this. The real signal is whether you can describe what the function does in one sentence without using “and”.

Reduce nesting with early returns

Deeply indented code is hard to read because you must hold every condition in your head simultaneously.

// Nested
function getDiscount(user) {
  if (user) {
    if (user.isActive) {
      if (user.ordersCount > 10) {
        return 0.2;
      } else {
        return 0.1;
      }
    }
  }
  return 0;
}
// Guard clauses
function getDiscount(user) {
  if (!user) return 0;
  if (!user.isActive) return 0;
  return user.ordersCount > 10 ? 0.2 : 0.1;
}

Handle the exceptional cases first and exit. What remains is the main path, unindented and obvious. This pattern alone eliminates most of the arrow-shaped code that makes reviewers wince.

Name your conditions

A complex boolean expression inline is a puzzle. Assigned to a well-named variable, it is documentation.

// Before
if (user.age >= 18 && user.verified && !user.banned && user.credits > 0) { }

// After
const canPlaceOrder =
  user.age >= 18 && user.verified && !user.banned && user.credits > 0;

if (canPlaceOrder) { }

This costs one line and removes the need for the reader to reconstruct business rules from operators.

Comments: why, not what

A comment restating the code is noise that will eventually become a lie, because comments are not executed and therefore not maintained.

// Bad
// increment counter by one
counter++;

// Good
// The payment provider rate-limits to 5 req/sec, so we batch
// and space these out rather than firing in parallel.
await processInBatches(payments, 5);

Good comments capture the things the code cannot: why an unusual approach was chosen, what constraint forced it, which bug report this defends against, what will break if someone “simplifies” it. If you find yourself writing a comment to explain what the code does, try renaming things first — usually the comment becomes unnecessary.

Handle errors deliberately

Swallowed errors are a common junior pattern and one of the hardest things to debug later.

// Silence
try { await saveOrder(order); } catch (e) { }

// Slightly better but still wrong
try { await saveOrder(order); } catch (e) { console.log(e); }

// Deliberate
try {
  await saveOrder(order);
} catch (error) {
  logger.error("Failed to save order", { orderId: order.id, error });
  throw new OrderSaveError("Could not save order", { cause: error });
}

Catch an error only if you are going to do something about it: retry, add context, convert it to a user-facing message, or log it with enough detail to diagnose. If you cannot do any of those, let it propagate to something that can.

Avoid magic numbers and strings

if (user.role === 2) { }
setTimeout(refresh, 900000);

What is role 2? What is 900000? Named constants answer both questions permanently:

const ROLE = { USER: 1, ADMIN: 2, AUDITOR: 3 };
const REFRESH_INTERVAL_MS = 15 * 60 * 1000;

The multiplication is deliberate — 15 * 60 * 1000 shows its own units in a way that 900000 never will.

Know when not to abstract

Juniors who have absorbed “don’t repeat yourself” often over-correct, and the result is harder to work with than the duplication was.

Two pieces of code that look similar are not necessarily the same thing. If they happen to match today but change for different reasons, merging them creates a function with three boolean flags and a comment explaining when each applies. That is worse than two clear functions.

A workable heuristic is to wait for the third occurrence before abstracting. By then you can see which parts genuinely vary. Premature abstraction locks in a shape based on one example.

Small, honest commits

Clean code extends to history. A commit containing a feature, a refactor, a dependency bump and a formatting pass cannot be reviewed properly or reverted safely.

Separate mechanical changes from behavioural ones. If you need to rename a variable across forty files and also fix a bug, make two commits. The reviewer can then skim the rename and concentrate on the fix.

Let tools handle style

Indentation, quote style and semicolons are not worth a single minute of human discussion. Install a formatter and a linter, commit the configuration, and let them run automatically on save or on commit. Every argument settled by tooling is attention freed for questions that actually matter.

Use code review as the feedback loop

The fastest way to improve is to read comments on your own pull requests without defensiveness, and to read other people’s pull requests without being asked. The second habit is undervalued: seeing how a senior developer structures a problem teaches more than any article, including this one.

When you disagree with a review comment, ask what the reviewer is optimising for. Often they are protecting against a failure mode you have not encountered yet.

Frequently asked questions

How long should a function be?

There is no correct number. The useful test is whether it does one thing and whether you can name it accurately. A forty-line function with a single clear purpose is better than four ten-line functions that only exist to satisfy a limit.

Should I refactor code I did not write?

Improve what you touch, in the same area you are already changing, and keep it in a separate commit. Large unrequested rewrites of working code are usually unwelcome, because they create review burden and merge conflicts for everyone else.

Is clean code slower to write?

Marginally, on the first pass. The payback comes at the first bug, the first change in requirements, and the first time someone else has to read it. On anything that lives longer than a week, it is faster overall.

What if my team’s codebase is messy?

Follow the existing conventions rather than introducing a second style — inconsistency is worse than an imperfect convention. Raise improvements gradually, with examples, and apply them to new code first.

Conclusion

Clean code as a junior comes down to a handful of habits: name things accurately, keep functions to one responsibility, exit early instead of nesting, name complex conditions, comment the reasoning rather than the syntax, handle errors on purpose, avoid magic values, and resist abstracting too soon. None of these require seniority. Applied consistently, they are most of what separates code that gets approved from code that gets sent back.

Leave a Reply

Your email address will not be published. Required fields are marked *