Ninja code

This whole article is a joke. Read it that way.

The “programmer ninjas” of legend supposedly used the tricks below to sharpen the minds of whoever maintained their code afterward. Code reviewers hunt for these moves in take-home assignments. And every so often a fresh developer stumbles into them so naturally that they out-ninja the ninjas.

Read carefully and see which one you are: the ninja, the novice, or the reviewer who has to clean up after both.

Brevity is the soul of wit

Make the code as short as humanly possible. Show everyone how clever you are. Let obscure language features do the talking.

Here’s a real ternary, nested inside another ternary:

// the sort of line that lurks in a popular utility
qty = qty ? qty > stock ? stock : qty : 1;

Compact, isn’t it? Anyone who lands on this line and needs to know what qty ends up being is in for a long afternoon. Eventually they’ll come to you for the answer, and you get to explain that shorter is always better.

Except it isn’t. Short and clear are different goals that only sometimes point the same way. Let’s unpack what that line actually does — the same logic, spelled out:

// same behavior, readable
if (qty > stock) {
  // more was requested than we have; cap it at the stock on hand
  qty = stock;
} else if (qty) {
  // truthy amount that fits in stock: keep it as-is
  qty = qty;
} else {
  // qty was 0, null, undefined, NaN, ''... treat as 1
  qty = 1;
}
nested ternary
qty ? ( qty>stock ? A : B ) : C
parse left→right, backtrack, match each ? to its : — all in your head
if / else
if (qty>stock) A
else if (qty) B
else C
read top→bottom, discard each branch once you pass it
Reading a nested ternary means holding every branch in your head at once. Reading an if/else lets you drop each branch as you rule it out.

One-letter variables

Another route to shorter code: name everything with a single letter. a, b, c.

A one-letter variable vanishes into the source like a ninja into the trees. Nobody can grep for it usefully — search a and the editor lights up half the file. And even if someone finds the declaration, a tells them nothing about what it holds.

There’s one twist for the truly committed. A real ninja never uses i for a for counter, because everyone recognizes i. Pick something with no convention behind it, like x or y. The move lands best when the loop body runs a page or two — by the time a reader is deep inside, they’ve forgotten that x was the counter.

Back in reality: single letters throw away the one cheap piece of documentation a name can carry. i, j, k for loop indices and x, y for coordinates are fine because the convention is the meaning. Everywhere else, spend the extra characters.

Use abbreviations

If the team bans one-letter names and vague nouns, don’t give up — abbreviate. Chop the vowels, drop the endings:

  • listlst
  • userAgentua
  • browserbrsr
  • …and so on

Now only someone with strong intuition can read your identifiers. Consider it a filter: only the worthy get to touch your code.

The problem, of course, is that abbreviations are a private language. ua is common enough to survive, but brsr forces every reader to decode it, and different people will guess different expansions. The characters you save typing are paid back many times over in reading.

Soar high. Be abstract.

When you pick a name, reach for the most abstract word you can find: obj, data, value, item, elem.

  • The perfect variable name is data. Use it everywhere. Every variable holds data, after all.

    And when data is already taken? value is just as universal — every variable eventually holds a value.

  • Name a variable after its type: str, num

    Try it. A beginner might ask whether type-names are actually useful to a ninja. They are. The name still says something — string, number — so it looks informative. But an outsider reading the code discovers there’s no real information in it at all, and gives up on changing your carefully guarded logic.

    The type is easy to recover with a debugger. The meaning is not. Which string? Which number? What is it for?

    No way to know without a long meditation.

  • Ran out of abstract names? Just append a number: data1, item2, elem5.

The straight version: a name should answer “what is this, and what is it for?” data and value answer neither. str answers “what type” — which the language already tracks — while dodging the question that matters. Prefer names that describe the role: userInput, pendingOrders, retryCount.

let data = 30
30 what? days? dollars? a user id? the reader has to trace every use
let trialDays = 30
answered at the point of declaration — no tracing needed
Two variables, same value. One name tells you what to do with it; the other tells you nothing you didn't already know.

Attention test

Only a genuinely attentive programmer deserves to read your code. How do you test for attention?

One method: use near-identical names, like node and mode. Sprinkle both through the same function and mix them where you can.

Skimming becomes impossible — every read demands full focus. And when a typo swaps one for the other, the bug hides in plain sight, because the two names look correct side by side. Time for tea while you hunt it down.

The real point cuts the other way: names that differ by one character are a trap you set for yourself. node/mode, count/counts, value/values — pick names that look distinct at a glance so a misread is impossible.

Smart synonyms

Using different names for the same thing keeps life interesting and puts your creativity on display.

Function prefixes are the perfect canvas. If a function shows a message, prefix it display…displayMessage. If a different function also shows something on screen, prefix that one show…showName. Hint at a subtle distinction between “display” and “show” when there is none.

Then form a pact with your teammates. Maya prefixes screen-writers with display...; Raj uses render...; Priya uses paint.... Look how varied the codebase became.

Now the finishing move: for two functions that genuinely differ, use the same prefix. printPage(page) sends to a printer. printText(text) writes to the screen. Let a reader agonize over printMessage — printer or screen? For maximum effect, printMessage(message) should open a brand-new window instead.

In earnest: consistency is the whole value of a naming convention. If get* always means “fetch and return, cheaply”, a reader trusts it on sight. Break that and every prefix becomes a question instead of an answer.

chaotic — “write to screen”
displayMessage()
showName()
renderStatus()
paintLabel()
consistent
showMessage()
showName()
showStatus()
showLabel()
A convention only helps if it's applied uniformly. Left: same idea, four names — the reader can't rely on any of them. Right: one prefix per idea — the name predicts the behavior.

Reuse names

Declare a new variable only when there’s absolutely no alternative. Otherwise, reuse an existing one — just write a new value into it. Inside a function, try to work only with the parameters you were handed.

That makes it genuinely hard to know what a variable holds right now, or where that value came from. Think of it as intuition training for the reader. Anyone with weak intuition will have to trace the code line by line, following the value through every branch.

The advanced form: quietly swap the value for something similar partway through a loop or function.

function processConfig(settings) {
  // 20 lines of code working with settings

  settings = deepCopy(settings);

  // 20 more lines, now working with a *copy* of settings!
}

A teammate who wants to touch settings in the second half assumes it’s still the original. Only under the debugger do they discover they’ve been editing a copy the whole time.

lines 1–20
settings ──▶ original
→ settings = deepCopy(settings) →
lines 21–40
settings ──▶ copy
One name, two identities. Before the reassignment, settings points at the original; after, at a copy. The name never changes, so the reader never notices the switch.

The honest rule: one variable, one meaning, for its whole life. If a value takes on a new role, give it a new name — settings then settingsCopy. Reassigning a parameter to a transformed version of itself is a classic source of “but I set it right there!” bugs.

Underscores for fun

Prefix names with underscores: _name, __value. Ideally only you know what they signify. Better still, add them with no meaning at all — or a different meaning in each place.

Two birds, one stone. The code grows longer and noisier, and a teammate burns real time trying to reverse-engineer what the underscores mean.

For extra fragility, use the underscore in one spot and skip it on the same kind of name elsewhere. Inconsistency breeds future errors.

In practice, a leading underscore is a convention in some codebases — it flags “internal, don’t touch from outside”. That’s fine when it’s applied consistently and documented. Decorative or random underscores are just noise that every reader has to mentally strip.

Show your love

Let everyone admire how grand your entities are. Names like ultraNode, giantPanel, and sweetItem will surely enlighten the reader.

On one side, the name announces ultra.., giant.., sweet... On the other, it conveys zero detail. A curious reader may go looking for the hidden significance and spend an hour or two of paid time in contemplation.

Straight up: intensifier adjectives describe your feelings about the code, not the code. ultraNode and node refer to the same thing; the prefix only adds a word to read. Drop it.

Overlap outer variables

Reuse the same name inside and outside a function. No effort spent inventing anything new.

let account = fetchAccount();

function render() {
  let account = blankAccount();
  ...
  ...many lines...
  ...
  ... // <-- a programmer wants to work with account here and...
  ...
}

A programmer who jumps into render may not notice that the local account shadows the outer one. They start using account expecting the result of fetchAccount()… and the trap springs. Hello, debugger.

This is shadowing: an inner declaration with the same name hides the outer one for the length of the inner scope. It’s legal and sometimes even intentional, but when the two accounts mean different things, every reference inside the function is ambiguous to a human even though the engine is certain.

outer scope
account ──▶ fetchAccount()
render() scope
account ──▶ blankAccount() ◀ shadows outer
every account here means the inner box
Inside render(), the local `account` shadows the outer one. Any reference to `account` in that scope resolves to the inner box — the outer value is unreachable by name there.

Side-effects everywhere!

Some functions look like they change nothing. isReady(), checkPermission(), findTags(). You expect them to compute, look something up, and return an answer without disturbing anything outside — no “side effects”.

A lovely trick is to bolt a “helpful” extra action onto them.

The dazed look on a colleague’s face when a function named is.., check.., or find... turns out to mutate something — that will expand your horizons.

Another way to startle: return an unexpected shape.

Show some originality. Let checkPermission return not true/false but a rich object describing the check. Developers who wrote if (checkPermission(...)) will wonder why it never behaves — an object is always truthy. Tell them to read the docs, and hand them this article.

For real: names in the is/has/check/find family are a contract. is/has promise a boolean and no mutation. find promises a lookup that returns a result and touches nothing. Break the contract and every call site becomes a place a bug can hide.

namereader expectsreturns
isReady()no mutationboolean
checkPermission()no mutationboolean
findTags()no mutationthe found data
What each name prefix silently promises. Breaking the promise is what makes the surprise 'work'.

Powerful functions!

Don’t box a function into what its name suggests. Think bigger.

validateEmail(email) could, on top of checking the address, pop up an error message and prompt the user to type it again. The extra behavior shouldn’t be obvious from the name — and a true ninja hides it in the code too.

Folding several jobs into one function is also a shield against reuse.

Suppose another developer wants only to check an address, no message shown. Your all-in-one validateEmail won’t fit their need, so they’ll leave you and your meditation undisturbed.

The genuine principle here is the opposite: a function that does one thing is the one people can reuse. validateEmail should return a verdict; showing an error and re-prompting are separate responsibilities that belong to the caller. Bundle them and the function is only usable in the exact situation you first wrote it for.

bundled
validateEmail(email)
├ checks format
├ shows error UI
└ re-prompts user
reusable in 1 place
focused
isValidEmail(email) → boolean
showError(msg)
promptEmail()
caller composes as needed
One function, three jobs (hard to reuse) versus three focused functions the caller composes (reusable anywhere).

Summary

Every “piece of advice” above is drawn from real code — sometimes written by developers with plenty of experience, occasionally more than you have.

  • Adopt a few of these habits, and your code fills with surprises.
  • Adopt many, and the code becomes truly yours: nobody else will want to touch it.
  • Adopt all of them, and your code turns into a cautionary tale for the next generation.

Flip every joke and you get the actual checklist:

The through-line: write for the person who reads this next, including future you. Every trick above saves the author a few seconds and costs every future reader far more. Good code spends keystrokes to buy clarity.