Functions#
Functions are the unit of intent. A good function does one thing, says what it does through its name and signature, and can be understood without reading its body.
One responsibility#
- A function does one thing. If describing it needs the word "and", split it.
- Keep it small enough to hold in your head. Whether it fits on a screen is a better test than a line count.
- One level of abstraction per function — don't mix high-level orchestration with low-level detail in the same body.
Reuse before you build#
Before writing new logic, use what already exists — and build only what does not. This is DRY and one responsibility applied across the whole codebase, not just within one function.
- Prefer a built-in. If the language or runtime already does the job, use it instead of a hand-rolled version.
- Reuse an existing function instead of re-implementing it. If it is the weak link — too slow or imprecise on a hot path — fix it there so every caller benefits, rather than working around it.
- Prefer shared code you own before introducing a third-party dependency, especially in a shipped library or module. An external package is a lasting trust and maintenance commitment, so only take it when the capability is large or specialized enough that owning it would be the worse trade.
- Build it only when nothing fits — no built-in, no existing function, no shared code you own, and no justified trusted dependency. Size the build to the need: small logic lives inline where it is used; a larger, cohesive capability becomes its own module.
Signatures are contracts#
- Type the parameters and return value where the language allows. The signature documents intent and lets tooling catch misuse before it runs.
- Few parameters. A long parameter list signals a missing type or a function doing too much — group related arguments into an object.
- Avoid boolean parameters that select behavior (
render(true)). Split into two clearly named functions, or pass an explicit, named option. - Order parameters so the required ones come first — readers scan left to right.
Validate at the boundary#
- Reject bad input where it enters, before it travels deep into the call stack. A clear failure at the edge beats a baffling one three layers down. This is shift left applied to a single function.
- Inside a validated boundary, trust the data. At the edge, trust nothing.
Flow reads top to bottom#
- Return early. Put guard clauses for edge cases and exits at the top, then let the main path read straight down — no deep nesting.
- Prefer high-level constructs (iterate a collection directly) over manual, error-prone ones (index into it) when the index isn't needed.
- Handle every case. A branch over a closed set of values covers every member or carries an explicit default — an unhandled value is a silent failure.
Side effects at the edges#
- Prefer pure functions: the output depends only on the input, with no hidden state and no side effects. Pure functions are trivial to test and to reason about.
- Push side effects — I/O, mutation, network, time — to the edges of the system, and keep the core logic pure.
Gate destructive operations#
- Operations that create, change, or delete state offer a preview-or-confirm path before they act. Irreversible actions earn a deliberate gate. See Security.