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.
Related Concepts
- Meaningful Names - Small functions need clear names
- Single Responsibility - Applied at the function level
- Separation of Concerns - Small functions help separate concerns
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.