prefer-named-functions
Prefer Named Functions Over Anonymous Functions
Goal
When defining a function, prefer a named function over an anonymous one, regardless of the technology stack.
A named function is any callable whose definition is bound to an explicit, descriptive identifier visible at its definition site (a function declaration, a named function expression, an arrow function or lambda assigned to a const/val/let with a meaningful name, a named method, or a named local helper). An anonymous function is a callable defined inline without a stable, descriptive identifier — typically a lambda, arrow function, block, or function expression passed directly as an argument or returned without being bound to a name.
Named functions improve stack traces, code navigation, searchability, reuse, testability, and the reader's ability to understand intent without parsing the body. Anonymous functions are still appropriate when the stack expects them or when naming would actively hurt readability.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines a callback, handler, listener, mapper, predicate, comparator, reducer, effect, or task as an inline anonymous function
- assigns an anonymous function or lambda to a variable without a meaningful name
- passes an inline lambda or arrow function with non-trivial logic as an argument
- defines an event handler, route handler, middleware, or job as an anonymous function
- exports an anonymous function or anonymous default export from a module
- repeats the same anonymous function shape in multiple places that could share a named definition
The Rule
-
Default to a named function for every function you define.
- Use the language's idiomatic naming construct: function declaration, named function expression, named lambda binding, named method, or named local helper.
- Choose a name that describes the function's intent in domain terms, not its mechanics.
-
Bind anonymous shapes to a descriptive name when they have non-trivial logic.
- If a callback, handler, or lambda contains more than a single trivial expression, lift it to a named function or a named local binding before passing it.
- Prefer extracting it to module scope or a clearly named local constant over inlining it.
-
Avoid anonymous default exports and anonymous module-level functions.
- Export functions under explicit names so call sites, stack traces, and tooling can identify them.
- Do not rely on the importing module to give the function a name.
-
Use anonymous functions only when one of the documented exceptions applies.
- The exception must be specific and justifiable, not a stylistic default.
- When in doubt, name it.
When Anonymous Functions Are Appropriate
Anonymous functions are appropriate when at least one of these holds:
- The stack is designed around anonymous callables. The language, framework, or API is built such that anonymous functions are the natural, idiomatic, or required form. Examples include single-expression collection operations (
map,filter,reduce), trailing lambdas in DSLs, structural shorthand like Kotlin's trailing lambda, Ruby blocks, Scala for-comprehensions, SAM conversions, or framework hooks that explicitly expect inline closures. - The function body is a trivial, self-evident expression. A one-line transformation, projection, or predicate where a name would only restate the body (e.g.,
users.map(u => u.id),items.filter(i => i.active)). - Naming would actively hurt readability. Introducing a name forces the reader to jump elsewhere to understand a one-shot, locally scoped operation whose meaning is obvious from context.
- The callable is a single-use, locally scoped continuation. A short closure that captures local state, runs once, and has no meaning outside its call site.
- The framework or runtime requires an anonymous form. The API contract only accepts inline lambdas, blocks, or anonymous classes (e.g., certain reactive operators, effect runners, or platform callbacks where naming is impossible or unidiomatic).
- Project conventions consistently use anonymous functions for this construct. The codebase has an established, deliberate convention and switching to named forms would introduce inconsistency or friction.
In all these cases, keep the anonymous body small and focused. If it grows beyond a trivial expression, lift it to a named function.
Examples
Prefer this:
function isActiveAdult(user: User): boolean {
return user.isActive && user.age >= 18;
}
const activeAdults = users.filter(isActiveAdult);
def is_active_adult(user: User) -> bool:
return user.is_active and user.age >= 18
active_adults = [u for u in users if is_active_adult(u)]
fun isActiveAdult(user: User): Boolean = user.isActive && user.age >= 18
val activeAdults = users.filter(::isActiveAdult)
Avoid this when the body is non-trivial:
const activeAdults = users.filter((u) => {
const meetsAge = u.age >= 18;
const isVerified = u.verifiedAt !== null;
return u.isActive && meetsAge && isVerified;
});
export default (req, res) => {
// anonymous default export — invisible in stack traces and imports
};
Anonymous is fine when the body is trivial and idiomatic:
const ids = users.map((u) => u.id);
const active = users.filter((u) => u.isActive);
ids = [u.id for u in users]
val ids = users.map { it.id }
Anonymous is fine when the stack expects it:
button.setOnClickListener { view -> handleClick(view) }
useEffect(() => {
subscribe();
return () => unsubscribe();
}, []);
Detection Workflow
-
Identify anonymous callables in the changed or reviewed code.
- Look for inline lambdas, arrow functions, function expressions, blocks, anonymous classes, and anonymous default exports.
- Note any callable whose definition site has no descriptive identifier.
-
Classify each anonymous callable.
- Trivial single-expression body in an idiomatic collection or framework call: leave as is.
- Required by the framework or runtime contract: leave as is.
- Established project convention: respect it.
- Otherwise: candidate for renaming or extraction.
-
Check stack traces, logs, and tooling impact.
- Anonymous functions often appear as
<anonymous>,lambda,<lambda>, or generated names in errors and profilers. - If the function participates in async flows, error reporting, or hot paths, prefer a name to aid debugging.
- Anonymous functions often appear as
-
Look for repeated anonymous shapes.
- The same anonymous predicate, mapper, or handler used in multiple places is a strong signal to extract a single named function.
Writing or Changing Functions
-
Name first.
- When you write a new function, give it an explicit, descriptive name in the language's idiomatic form.
- Place it at the smallest scope where it is reusable and discoverable.
-
Extract when bodies grow.
- If an anonymous callback grows beyond a trivial expression, extract it to a named function before continuing.
- Keep the call site readable:
users.filter(isActiveAdult)over a multi-line inline lambda.
-
Replace anonymous default exports with named exports.
- Export the function under an explicit name and let the importer use the same name.
-
Preserve idiomatic anonymous use.
- Do not rewrite
users.map((u) => u.id)into a named helper just to satisfy the rule. - Do not name closures that the framework expects to be anonymous and that have no meaning outside the call site.
- Do not rewrite
-
Match project conventions.
- If the codebase has a consistent, deliberate convention for a given construct (e.g., always-anonymous route handlers in a specific framework), follow it.
- If the convention is accidental or inconsistent, prefer named forms going forward.
Review Questions
When reading or reviewing code, ask:
- Is this callable anonymous when a named function would work?
- Does its body go beyond a trivial, self-evident expression?
- Would a name help stack traces, navigation, search, or reuse?
- Is the anonymous form genuinely required or strongly favored by the stack, framework, or local readability?
- Is the same anonymous shape repeated in multiple places that could share a named definition?
- Is this an anonymous default export or anonymous module-level function that should be named?
If a callable is anonymous without a clear reason, apply this skill.
Report the Outcome
When finishing the task:
- state which anonymous callables were identified, named, or extracted
- state which anonymous callables were intentionally left anonymous and why (stack idiom, trivial body, framework requirement, or project convention)
- state whether project conventions or stack constraints influenced the choice
More from code-sherpas/agent-skills
neverthrow-return-types
Require `neverthrow`-based return types in TypeScript and JavaScript code whenever the surrounding technology allows it. Use when creating, refactoring, reviewing, or extending standalone functions, exported module functions, class methods, object methods, service methods, repository methods, and similar APIs that should expose explicit success and failure result types in their signatures. Prefer `Result<T, E>` for synchronous code and `ResultAsync<T, E>` for asynchronous code. Only skip a `neverthrow` return type when a framework, library, runtime interface, or externally imposed contract is incompatible and requires a different return shape.
16neverthrow-wrap-exceptions
Capture exceptions and promise failures with `neverthrow` instead of hand-written `try/catch` in TypeScript and JavaScript code. Use when wrapping synchronous functions that may throw, promise-returning functions that may throw before returning, existing `PromiseLike` values that may reject, or third-party APIs such as parsers, database clients, HTTP clients, file-system helpers, serializers, and SDK calls. Prefer `Result.fromThrowable` for synchronous throwers, `ResultAsync.fromThrowable` for promise-returning functions that may throw or reject, and `ResultAsync.fromPromise` when you already have a `PromiseLike` value in hand. Only keep `try/catch` when the language construct, cleanup requirement, or framework boundary truly requires it.
11atomic-design
Create or update web UI components with a strict reuse-first workflow. Use when building, refactoring, restyling, or extending frontend or template components while minimizing raw DOM or HTML by reusing or generalizing existing components first.
10business-logic
Identify, interpret, review, or write business logic in code. Use when an agent needs to decide whether code expresses business rules, business algorithms, or business workflows, or when it must implement, preserve, or refactor code that creates, stores, or transforms data according to real business policies.
7immutable-domain-entities
Require the immutable design pattern for domain entities. Use when an agent needs to create, modify, review, or interpret domain entities and should preserve identity while expressing state changes through new immutable instances. Domain entities must be modeled as immutable classes, not as plain type aliases or interfaces paired with standalone functions.
7update-agent-skills
Update agent skills installed with the `skills` CLI. Use when asked to refresh installed skills, keep a project's skills current, or troubleshoot cases where `npx skills update` reports that everything is up to date. For project-scoped installs, a no-change update must immediately run the bundled reinstall script so tracked skills from `skills-lock.json` are reinstalled without extra investigation.
7