Skip to content
aipatterns.dev

Code smellBloaters

Long Method

A function that does several jobs in a row and has grown too long to read, name or test as one unit.

Also known as
God Function

Signs

  • You have to scroll to see the whole function, or you need to keep more than a handful of local variables in your head at once.
  • Blank lines and comments split the body into "paragraphs" such as // validate, // compute totals, // send email.
  • The name is vague (process, handle, run) because no specific name covers everything it does.
  • Tests for it need large fixtures, and each test asserts on many unrelated outputs.
  • Diffs touch the same function for unrelated reasons: a tax change and a logging change land in the same 150 lines.

Why it happens

Adding a line to an existing function always feels cheaper than creating a new one. Each change is small and reasonable, so nobody notices the function crossing the line from "a step" to "a workflow". Local variables make it worse: once five temporaries are shared across the body, pulling out a piece means deciding what to pass in and out, so people keep appending instead.

Example

An order-placement handler that validates, prices, persists and notifies in one block.

placeOrder.ts
async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise<string> {
  // validate
  if (!req.items.length) throw new Error("empty order");
  for (const item of req.items) {
    if (item.qty <= 0) throw new Error(`bad qty for ${item.sku}`);
  }

  // price
  let subtotal = 0;
  for (const item of req.items) {
    const product = await db.products.get(item.sku);
    subtotal += product.priceCents * item.qty;
  }
  let discount = 0;
  if (req.coupon === "SPRING10") discount = Math.round(subtotal * 0.1);
  const tax = Math.round((subtotal - discount) * 0.08);
  const total = subtotal - discount + tax;

  // persist
  const id = await db.orders.insert({ ...req, subtotal, discount, tax, total });

  // notify
  await mailer.send(req.email, "Order received", `Order ${id}: $${(total / 100).toFixed(2)}`);
  return id;
}

After extracting each paragraph into a named function, the top-level function reads as a table of contents.

placeOrder.ts
async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise<string> {
  assertValid(req);
  const price = await priceOrder(req, db.products);
  const id = await db.orders.insert({ ...req, ...price });
  await sendConfirmation(mailer, req.email, id, price.total);
  return id;
}

function assertValid(req: OrderRequest): void {
  if (!req.items.length) throw new Error("empty order");
  const bad = req.items.find((i) => i.qty <= 0);
  if (bad) throw new Error(`bad qty for ${bad.sku}`);
}

async function priceOrder(req: OrderRequest, products: ProductRepo): Promise<Price> {
  const subtotal = await subtotalOf(req.items, products);
  const discount = couponDiscount(req.coupon, subtotal);
  const tax = Math.round((subtotal - discount) * TAX_RATE);
  return { subtotal, discount, tax, total: subtotal - discount + tax };
}

Treatment

Payoff

  • Each function has one name that tells the truth, so callers and reviewers read intent, not mechanics.
  • Small functions are testable in isolation and reusable elsewhere.
  • Duplicate fragments become visible once pieces have names.
  • Changes land in one small function, so diffs and merge conflicts shrink.

When to ignore

  • The function is long but flat and linear, such as a configuration table, a route list or a generated mapping. Splitting it adds jumps without adding meaning.
  • A hot loop where a profiler shows that call overhead matters and the compiler does not inline. This is rare; measure first.
  • Test functions that set up one scenario end to end. Arrange-act-assert in one place can be clearer than helper soup.

Agent checklist

  • Tests cover the function before you split it; add characterization tests if they don't.
  • Each extracted function has a name specific enough that the comment above the old paragraph can be deleted.
  • Extracted functions take the data they need as arguments and do not reach back into the caller's locals through closures or shared mutable state.
  • The original function now reads as a short sequence of steps at one level of abstraction.
  • Behavior is unchanged: the same tests pass with no edits to their assertions.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.