Small Functions

Functions should do one thing, do it well, and do it only.

Overview

Small functions are the building blocks of readable code. A function that fits on a screen, does one thing clearly, and has a descriptive name is far easier to understand, test, and maintain than a large function that handles multiple responsibilities. The goal isn’t an arbitrary line count—it’s clarity of purpose.

Key Principles

Do One Thing

A function should perform a single, well-defined task. If you can extract another function from it and give that extraction a meaningful name, the original function was doing more than one thing.

// Doing multiple things
function processOrder(order: Order): void {
  // Validate
  if (!order.items.length) throw new Error('Empty order');
  if (!order.customerId) throw new Error('No customer');
 
  // Calculate total
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
 
  // Apply discount
  if (order.couponCode) {
    const discount = lookupDiscount(order.couponCode);
    total *= 1 - discount;
  }
 
  // Save
  database.save({ ...order, total });
 
  // Notify
  emailService.send(order.customerId, 'Order confirmed');
}
 
// Each doing one thing
function processOrder(order: Order): void {
  validateOrder(order);
  const total = calculateOrderTotal(order);
  saveOrder(order, total);
  notifyCustomer(order.customerId);
}

One Level of Abstraction

Statements within a function should all be at the same level of abstraction. Mixing high-level operations with low-level details makes code harder to follow.

// Mixed abstraction levels
function renderPage(): string {
  const user = getCurrentUser();
  let html = '<!DOCTYPE html><html>';
  html += '<head><title>' + escapeHtml(user.name) + '</title></head>';
  const content = loadPageContent();
  html += '<body>' + content + '</body></html>';
  return html;
}
 
// Consistent abstraction
function renderPage(): string {
  const user = getCurrentUser();
  const content = loadPageContent();
  return buildHtmlDocument(user.name, content);
}

Minimize Arguments

Fewer arguments make functions easier to understand and test. Zero arguments is ideal; one or two is acceptable; three should be avoided when possible. When you need to pass many values, consider whether they form a natural object.

// Too many arguments
function createUser(
  firstName: string,
  lastName: string,
  email: string,
  phone: string,
  address: string
): User { ... }
 
// Grouped into a coherent object
function createUser(userData: UserCreationRequest): User { ... }

No Side Effects

A function named checkPassword shouldn’t also initialize a session. Hidden side effects break trust—readers expect a function to do what its name says and nothing more.

Example

A common pattern is extracting the body of a loop:

// Before: Loop body mixed with iteration
function printOwing(invoice: Invoice): void {
  let outstanding = 0;
 
  console.log('**************************');
  console.log('***** Customer Owes ******');
  console.log('**************************');
 
  for (const order of invoice.orders) {
    outstanding += order.amount;
  }
 
  const today = new Date();
  invoice.dueDate = new Date(today.setDate(today.getDate() + 30));
 
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
 
// After: Each step is a focused function
function printOwing(invoice: Invoice): void {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}
 
function printBanner(): void {
  console.log('**************************');
  console.log('***** Customer Owes ******');
  console.log('**************************');
}
 
function calculateOutstanding(invoice: Invoice): number {
  return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
 
function recordDueDate(invoice: Invoice): void {
  const today = new Date();
  invoice.dueDate = new Date(today.setDate(today.getDate() + 30));
}
 
function printDetails(invoice: Invoice, outstanding: number): void {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

Common Mistakes

Extracting too aggressively - If a three-line helper is only used once and requires passing five parameters, the extraction may not be worthwhile.

Naming extracted functions poorly - doPartOne() isn’t better than leaving the code inline. The name must convey meaning.

Creating functions that are “mostly” single-purpose - Adding “just one more thing” to a function begins the slide back toward large, tangled code.

Obsessing over line counts - The goal is clarity, not hitting a magic number. A clear 20-line function is better than five confusing 4-line functions.

References

  • Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Chapter 3: Functions.
  • Fowler, M. (2018). Refactoring: Improving the Design of Existing Code. Extract Function pattern.