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;
}
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:
list→lstuserAgent→uabrowser→brsr- …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
datais already taken?valueis 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.
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.
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.
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.
account here means the inner boxSide-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.
| name | reader expects | returns |
|---|---|---|
| isReady() | no mutation | boolean |
| checkPermission() | no mutation | boolean |
| findTags() | no mutation | the found data |
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.
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.