Rule
Keep functions concise.
Long functions are difficult to understand, test, and maintain.
Supported languages: 45+Introduction
Functions spanning hundreds of lines mix multiple responsibilities, making it hard to understand what the function does without reading every line. Long functions typically handle multiple concerns like validation, business logic, data transformation, and error handling all in one place. This violates the single responsibility principle and creates code that's difficult to test, debug, and modify without breaking existing behavior.
Why it matters
Code maintainability: Long functions require developers to hold more context in their head to understand behavior. Modifying one part risks breaking another because all logic is intertwined. Bug fixes become risky as unintended side effects are hard to predict.
Testing complexity: Testing a 200-line function means covering all possible code paths in one test, requiring complex setup and numerous test cases. Smaller functions can be tested independently with focused unit tests, making test suites faster and more reliable.
Code examples
❌ Non-compliant:
async function processOrder(orderData) {
if (!orderData.items?.length) throw new Error('Items required');
if (!orderData.customer?.email) throw new Error('Email required');
const subtotal = orderData.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
const tax = subtotal * 0.08;
const total = subtotal + tax + (subtotal > 50 ? 0 : 9.99);
const order = await db.orders.create({
customerId: orderData.customer.id,
total: total
});
await emailService.send(orderData.customer.email, `Order #${order.id}`);
await inventory.reserve(orderData.items);
return order;
}
Why it's wrong: This function handles validation, calculation, database operations, email, and inventory. Testing requires mocking all dependencies. Any change to tax logic or validation requires modifying this entire function.
✅ Compliant:
function validateOrder(orderData) {
if (!orderData.items?.length) throw new Error('Items required');
if (!orderData.customer?.email) throw new Error('Email required');
}
function calculateTotal(items) {
const subtotal = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
return subtotal + (subtotal * 0.08) + (subtotal > 50 ? 0 : 9.99);
}
async function createOrder(customerId, total) {
return await db.orders.create({ customerId, total });
}
async function processOrder(orderData) {
validateOrder(orderData);
const total = calculateTotal(orderData.items);
const order = await createOrder(orderData.customer.id, total);
// Non-critical operations in background
emailService.send(orderData.customer.email, `Order #${order.id}`).catch(console.error);
return order;
}Why this matters: Each function has one clear responsibility. validateOrder() and calculateTotal() can be tested independently without mocks. createOrder() isolates database logic. Email and inventory operations don't block order creation, and failures are handled separately.
Conclusion
Evolve APIs through additive changes: add new fields, add new endpoints, add optional parameters. When breaking changes are unavoidable, use API versioning to run old and new versions concurrently. Deprecate old fields with clear timelines and migration guides before removing them.
.avif)
