This page looks best with JavaScript enabled

JavaScript Tips: How to Write Clean Functions

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

JavaScript How to Write Clean Functions


In this post, we’ll cover simple tips for writing clean functions in JavaScript. Following these tips will make your code more maintainable and easier to read and understand.

Let’s get started!

Watch out for hidden side effects

“Pure” functions are another way of describing this rule. Pure functions always return the same result, given the same inputs. They also don’t have any hidden side effects, meaning they don’t modify their inputs or data outside their function scope. A good portion of any application can usually be written this way.

Of course, I am not saying that all side effects are bad and should be avoided. Your program has to have side effects. Otherwise, what’s the point of it?

The trick here is to bubble up your side effects higher in your application’s code. For example, if you’re writing a function that fetches data from an API, that’s a side effect and you want to make it more obvious and avoid burying it deep in your function call stack.

What if your function has to make changes to its input? You can still keep it pure by creating a new copy of the input object instead of modifying it in place. Let’s compare two examples:

Bad:

1
2
3
4
function addProperty(obj, key, value) {
    obj[key] = value;
    return obj;
}

Good:

1
2
3
4
5
function addProperty(obj, key, value) {
    const newObj = { ...obj }
    newObj[key] = value;
    return newObj;
}

The first function violates the rule of avoiding side effects since it modifies the input object. The second function keeps the input object untouched and returns a new object with the desired property added.

The second approach is a much safer way to write functions.

Handling functions with lots of parameters

There are a couple of things you can do to improve the UX of functions with many parameters. A simple approach is to wrap all of the parameters with an object. Here’s an example

Bad:

1
2
3
function addUser(firstName, lastName, age, city, state, zipCode) {
    // ... do something with the user info
}

Good:

1
2
3
function addUser({ firstName, lastName, age, city, state, zipCode }) {
    // ... do something with the user info
}

The second function will have better IDE support. It’s also easy to add new parameters without changing the function signature. And of course you will no longer have to worry about the order of the parameters.

Currying

Currying is a foundational technique in functional programming. It’s used for creating functions that take multiple arguments, one at a time.

You can use currying to create new functions from existing ones with fewer arguments. It’s useful when you need to create a lot of similar functions with different values.

Different functional programming libraries (ex Ramda.js ) provide utilities to curry functions with a different number of parameters. With that said, in many cases, you can implement a simple curry function yourself.

One good example of a simple currying approach is providing event handlers to your React components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export default function SampleComponent({ onValueChange }) {
  const handleChange = (key) => {
    return (e) => onValueChange(key, e.target.value)
  }

  return (
    <form>
      <input onChange={handleChange('name')} />
      <input onChange={handleChange('email')} />
      <input onChange={handleChange('phone')} />
    </form>
  )
}

In the code above, we have a curried the handleChange function that takes a key and returns an event handler function. Whenever the input value changes, the onValueChange callback is invoked already prefilled with the key thanks to closure.

Now we don’t have to create separate event handlers for each input. We can curry the handleChange function with a different key and reuse it multiple times.

Do one thing

Sometimes I see functions like these:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const createMeal = ({ type, addExtra }) => {
    const request = type === "appetizer" ? createAppetizer : createMainCourse;

        return request((response) => {
            if (addExtra) {
                if (type === "appetizer") {
                    response.dressing = "ceaser";
                } else if (type === "main") {
                    response.sides = "fries";
                }
                
                return response;
        }
    });
};

createMeal({ type: "main", addExtra: true });
createMeal({ type: "appetizer", addExtra: false });

This function is ripe for splitting. There’s no reason it needs to support two different use-cases. It would be much better to have two smaller, more specialized functions instead.

Usually, these functions don’t start off like this. They start small and simple, then as new requirements come in, they get more and more convoluted. So it’s important to take a step back from time to time and ask yourself if a function is doing too much.

The code above is much better rewritten like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const createAppetizer = (addExtra) => {
    return createAppetizerAPI().then((response) => {
        if (addExtra) {
            response.sides = "fries";
        }
    });
};

const createMainCourse = (addExtra) => {
    return createMainCourseAPI().then((response) => {
        if (addExtra) {
            response.dressing = "ceaser";
        }
    });
};

The intent of each function is much clearer now, and they are easier to reuse.

This splitting approach works best for functions where we know the arguments ahead of time. On the other hand, functions with dynamic arguments, such as user input, might be more challenging to split this way.

A good way to figure out if your function is doing too much is to ask yourself how easy it would be to write tests for it.

In general, the easier it is to test a function, the better.

Tune your app’s decision tree

Here’s another way of writing functions that I often see:

1
2
3
4
const filterOutMeals = ({ meals, customerOrder }) => {
    if (customerOrder.cancelled) return [];
    return meals.filter((meal) => !meal.isCold);
};

Doesn’t seem too bad, but the early return doesn’t need to be in this function. This goes back to the idea of pushing the important decision and side-effects up in the call stack to make them more visible.

Here’s how I’d rewrite that code:

1
2
3
4
5
const filterOutMeals2 = ({ meals }) => {
    return meals.filter((meal) => !meal.isCold);
};

const meals = customerOrder.cancelled ? [] : filterOutMeals2({ meals });

This way, the intent of this piece of code is clearer since the decision to avoid filtering if customer cancelled the order is higher up in the decision tree.

Conclusion

Writing clean functions is important for several reasons. It makes your code more readable, reusable, and easier to test.

In this post, we covered some simple tips for writing clean functions in JavaScript. Hopefully, you found these tips helpful! If you’re interested, a while ago I wrote a separate blog post on the proper strategy for refactoring your code .

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.