Functions
Cohesion, parameters, return values, and the right level of abstraction
Functions
The function is the smallest unit of behavior most programs reason about. The properties of a single function — what it depends on, what it changes, what it returns — propagate to every system built on top of it.
A Function Should Do One Thing
The most repeated rule in the literature, and the most often misapplied. "One thing" is defined relative to the function's level of abstraction:
A function does one thing if every step inside it is one level of abstraction below the function's name.
// Mixed levels — does several things
function processOrder(order) {
if (!order.items.length) throw new Error('empty');
const total = order.items.reduce((sum, i) => sum + i.price * i.qty, 0);
db.query('INSERT INTO orders ...');
fetch('/email', { ... });
}
// Consistent level — does one thing
function processOrder(order) {
validate(order);
const total = computeTotal(order);
persist(order, total);
notifyCustomer(order);
}The second version reads as a description of what happens. The reader can stop there or descend into any helper as needed.
Cohesion
Cohesion measures how strongly the elements of a function (or module) belong together. From strongest to weakest:
| Type | Description | Verdict |
|---|---|---|
| Functional | All steps contribute to a single, well-defined task | Target |
| Sequential | Output of one step feeds the next | Acceptable |
| Communicational | Steps operate on the same data | Acceptable |
| Temporal | Steps happen at the same time (e.g. init()) | Tolerable; split internals |
| Procedural | Steps follow a procedure but are otherwise unrelated | Refactor |
| Logical | A flag selects between alternatives | Split into separate functions |
| Coincidental | Steps grouped for no clear reason | Refactor immediately |
Logical cohesion is the most common failure mode in growing code:
// Logical cohesion — flag controls behavior
function user(action, id, data) {
if (action === 'create') { ... }
else if (action === 'update') { ... }
else if (action === 'delete') { ... }
}
// Functional cohesion — separate functions
function createUser(data) { ... }
function updateUser(id, data) { ... }
function deleteUser(id) { ... }Parameters
Count
Empirically, functions with more than three or four parameters become hard to read and easy to call incorrectly. When the count grows:
- Group related parameters into an object that names the group:
drawCircle(x, y, r, strokeColor, fillColor, strokeWidth) // becomes drawCircle({ center: { x, y }, radius: r, stroke: { color, width }, fill }) - Split the function if the parameters fall into independent clusters that are used at different times.
Order
Adopt a convention and apply it everywhere:
- Inputs first, outputs last.
- Required parameters first, optional last.
- For
(source, target)pairs, agree on a direction (copy(src, dst)) and keep it.
Avoid flag arguments
render(component, true) // true means what?
render(component, false)A boolean flag almost always indicates the function is doing two things. Split it:
renderWithDebugInfo(component)
renderProduction(component)Use named arguments where the language allows
In languages with keyword arguments (Python, Kotlin, Swift) or destructuring (JavaScript, TypeScript), prefer named call sites over positional ones once you have more than two arguments. The cost at the call site is small; the readability gain is large.
Do not mutate inputs
A function that silently modifies its arguments is a hazard. Either return a new value, or make the mutation obvious in the name (appendTo(target, item) rather than add(item, target)).
Return Values
Be consistent about what "no result" means
Pick one convention per function and document it:
- Return an empty collection rather than
nullfor "no matches." - Use
Optional/Maybe/ nullable types to make absence explicit in the type signature. - Use the language's idiomatic error channel (exceptions,
Result, error returns) for failures, not sentinel values.
Command-Query Separation
A function should either:
- Query state and return information (no side effects), or
- Command the system to change (no meaningful return value, or a small status).
Mixing the two makes the function impossible to call without thinking about both axes:
// Mixed — returning the deleted user is convenient until it bites you
const user = deleteUser(id);
// Separated
const user = getUser(id);
deleteUser(id);Pragmatic exception: idiomatic patterns like pop(), compareAndSet(), or fluent builders break the rule deliberately. Break it deliberately, not by accident.
Length
There is no magic line count. The useful rule is:
A function is too long when you cannot read it in one sitting and remember what it does.
In practice, that tends to mean:
- Most functions fit in well under a screen.
- Public API entry points may be longer if the body is a sequence of named subroutines and reads top-to-bottom as documentation.
- A function spanning hundreds of lines is almost always doing several things — split by extracting named subfunctions until each one fits the abstraction-level rule.
Length is a symptom, not the disease. The disease is mixed abstraction levels and weak cohesion.
Side Effects
Be explicit about what a function changes outside its own scope: parameters, instance state, global state, files, network, time. The fewer side effects per function, the easier the function is to test and reason about.
When a side effect is necessary, give it a name:
saveDraft()is honest about persisting.formatDate()had better not write to a database.
Preconditions and Postconditions
Make the contract explicit at the boundary:
/**
* Returns the user with the given ID.
*
* @param id must be a non-empty string
* @throws NotFoundError if no user exists with that ID
* @returns the user record; never null
*/
function getUser(id: string): User { ... }Encode what the type system can: required vs optional, nullable vs non-nullable, narrow types over string. Use assertions for what the type system cannot, and documentation for what neither can.
Pre-Commit Checklist
- Does the function name describe a single thing at one abstraction level?
- Could a reader skim the body and understand the flow without descending into helpers?
- Are there fewer than four parameters, or are they grouped into a parameter object?
- Does it either query or command, but not both?
- Are side effects either avoided or made obvious in the name?
- Does the type signature carry as much of the contract as the language allows?