Keep It Short and Sweet: The Art of Small Functions

Think about following a recipe. Would you rather have a single, massive paragraph detailing every step from chopping onions to plating the final dish, or a clean, numbered list of simple instructions? Most of us would choose the list. It's easier to follow, harder to mess up, and you can easily see the overall structure of the cooking process.

The same principle applies to our code. Long functions that do many different things are like that giant paragraph—hard to read, difficult to test, and a nightmare to debug. The goal is to write small functions that do one thing well.

The Problem: A Function That Does Everything

Let's look at a classic example: a function that handles user registration. This single function tries to do it all: validate input, check the database, create a user, and send an email.

// 👎 Bad: This function is a monolith
async function processUserRegistration(userData: UserData) {
  // 1. Validate email
  if (!userData.email || !userData.email.includes('@')) {
    throw new Error('Invalid email');
  }

  // 2. Validate password
  if (!userData.password || userData.password.length < 8) {
    throw new Error('Password too short');
  }

  // 3. Check if user already exists
  const existingUser = await db.findUserByEmail(userData.email);
  if (existingUser) {
    throw new Error('Email already exists');
  }

  // 4. Hash the password
  const hashedPassword = await bcrypt.hash(userData.password, 10);

  // 5. Save the new user to the database
  const user = await db.createUser({
    ...userData,
    password: hashedPassword,
  });

  // 6. Send a welcome email
  await emailService.sendWelcomeEmail(user.email);

  // 7. Log the event
  await analytics.track('user_registered', { userId: user.id });

  return { success: true, userId: user.id };
}

This function is doing at least seven different things! If something goes wrong, you have to dig through this whole block of code to find the problem. Testing it is also a pain because you need to set up a database, an email service, and an analytics tracker just to test the validation logic.

The Solution: A Team of Specialists

Let's break this monolith into smaller, more focused functions. The main function will become a coordinator, delegating tasks to other functions that are specialists in their domain.

// 👍 Good: A high-level summary of the process
async function processUserRegistration(userData: UserData) {
  validateInput(userData);
  await ensureUserDoesNotExist(userData.email);

  const user = await createUser(userData);
  await sendPostRegistrationEmails(user);
  await trackRegistrationEvent(user);

  return { success: true, userId: user.id };
}

// --- Helper Functions ---

function validateInput(userData: UserData) {
  // ... validation logic for email, password, etc.
}

async function ensureUserDoesNotExist(email: string) {
  // ... logic to query database and throw if user exists
}

async function createUser(userData: UserData) {
  // ... logic to hash password and save user to db
  return newUser;
}

// ... and so on for email and analytics.

Look at how readable processUserRegistration is now! It reads like a list of steps, telling a clear story.

  • Readability: You can understand the entire registration flow without getting bogged down in implementation details.
  • Testability: You can test validateInput completely separately from the database or email service. Each small function can be unit-tested in isolation.
  • Reusability: Need to validate user data somewhere else? No problem! The validateInput function is ready to be reused.

So, How Short is Short?

There's no magic number for function length (e.g., "never more than 10 lines!"). Instead of counting lines, ask yourself these questions:

  • Does this function do only one thing? Our new createUser function only creates a user. It doesn't validate or send emails. That's its single responsibility.
  • Is the code inside the function at the same level of abstraction? The refactored processUserRegistration is beautiful because all the lines are at a high level of abstraction. They describe what is happening, not how it's happening. The how is hidden inside the smaller functions.
  • Could I give it a better name? If you struggle to find a clear, precise name for your function (e.g., validateAndCreateUserAndSendEmail), it's a giant red flag that the function is doing too much.

Keep It Simple

The goal of writing short functions isn't just to follow a rule; it's to reduce cognitive load. By breaking complex problems into smaller, manageable pieces, you make your code easier for everyone (including yourself) to understand, maintain, and build upon.