Verbs and names
A function is an action, so its name starts with a verb. The verb comes from a closed lexicon, and every verb carries a contract that must not be broken.
Reading
find*May return null. Never throws — absence is a normal outcomeget*Guarantees a value. No value means it throws. No null in the typelist*Always a collection. An empty collection is neither an error nor nullcount*A number. Never nullThis one pair changes how code reads:
const track = await findTrack(trackId)
if (track === null) return makeFail('music.track.missing', 'Track not found')
const owner = getOwner(track)Without opening either function you can see that absence is routine for the first and a bug for the second. getOwner here means "an owner must exist, otherwise the data is broken".
Boundaries and storage
read* / write*Crossing an I/O boundary: network, disk, object storageload* / save*A whole aggregate through storage (the database)Separating read from load is not pedantry: the name tells you whether the call survives offline and whether it is worth caching.
Computation
make*A pure constructor of a value. No I/O, no clock, no randomnessderive*A pure computation from data you already holdresolve*Picking one option out of several by rulesparse* format*Parsing and presentation: input → output, no stateencode* decode*Changing representation without changing meaningnormalize*Reducing to a canonical formsign* verify* hash*Cryptographic transformationsChange
create* / delete*A domain mutation: changes state and emits an eventarchive* / restore*Reversible removal from circulationensure*Idempotently brings state to the required shape — calling twice changes nothingassert*Throws when an invariant is broken, otherwise void. Returns nothingapply*Applies a prepared change to a targetset*Assigns a value; nothing else happens downstreamSignals and lifecycle
emit*To the bus. The recipient is unknown and irrelevantsend*To a specific recipient — the recipient is part of the name: sendVerifyEmailstart* / stop*The lifecycle of a process or subsystemuse* with* render*UI code only: hooks, wrappers, renderingBanned verbs
Each one is banned because it states no action — it states that the author had not decided what the function does.
| Banned | Use instead |
|---|---|
handle* | Name the action: submitLogin, retryPayout, dropStaleSession |
process* | derive* / apply* / the precise transformation verb |
manage* | The function does more than one thing — split it |
do* perform* execute* run* | Name the action |
check* | is* if it returns an answer, assert* if it throws |
init* | start* for a process, make* for a value |
update* | save* / apply* / set* — say what actually happens |
fetch* retrieve* | read* (I/O) / load* (aggregate) / get* |
calculate* compute* | derive* |
generate* build* prepare* | make* |
setup* | ensure* / start* |
validate* | assert* (throws) / parse* (returns a parse) |
transform* convert* | derive* / format* / encode* |
determine* | resolve* |
trigger* | emit* / send* |
Word order: domain → qualifier → unit
urlOfCover
refreshTimeout
amounttrackCoverUrl
sessionRefreshTtlSec
payoutAmountCentsGeneral on the left, specific on the right. Names like these sort alphabetically into meaningful groups, and autocomplete on the first word surfaces the whole domain at once.
A number without a unit is an error
This is the one GRAIN rule that catches an entire class of production bugs: seconds handed to something that expected milliseconds.
| Suffix | Meaning |
|---|---|
*At | A point in time, always UTC |
*Ms *Sec *Min *Hours *Days | A duration |
*Bytes *Kb *Mb *Gb | A size |
*Cents | Money — always minor units, always an integer |
*Ratio | A fraction, 0..1 |
*Pct | A percentage, 0..100 |
*Count *Index | A count and a position |
*Px *Deg *Hz *Bpm *Db | Domain quantities |
Dimensionless values that need no suffix: x y z width height depth port page limit offset version priority. The list is closed — if your quantity is not on it, it has a unit.
Booleans — seven prefixes, no others
is · has · can · should · was · will · must
const isPublished = track.publishedAt !== null
const hasActiveSubscription = subscription.state === 'active'
const canPublish = isOwner || isAdminenabled, published, flag, status are not boolean names.
Words that say nothing
Banned as the last word of a name and as a whole name:
data info obj object temp tmp stuff misc thing things
res ret arr str num flag val result
helper handler manager wrapper util utilsuserData → user. trackInfo → track or trackSummary. parseResult → parsedTrack. If removing the empty word makes the name indistinguishable from another one in the same scope, then either those two things really are the same thing, or one of them needs a real name.
Abbreviations
Exactly these are allowed: id url uri db io rpc ttl utc api sql css html json jwt s3 ip dns cdn ui ms.
Everything else is spelled out: config not cfg, request not req, message not msg, error not err, index not idx, previous not prev.
Name length is proportional to scope. In a two-line callback, t is honest and fine. In an exported API there are no single-letter names.
Self-praise
enhanced advanced comprehensive ultimate robust powerful seamless smart intelligent optimized improved modern simple easy quick — banned in names. That is a judgement, not a description; it expires the moment it is committed and almost always means "I could not find what makes this version different from the last one".