DEV Community

Cover image for 4 Reasons You Should Avoid Function's Side-Effects
Matti Bar-Zeev
Matti Bar-Zeev

Posted on

4 Reasons You Should Avoid Function's Side-Effects

Whether you like him or not you have to accept the fact that Robert C. Martin (a.k.a Uncle Bob) is one of the godfathers of software development as we know it today.
A fact that uncle Bob likes to give as an introduction to his talks is that software developers multiply their number every 5 years and this means that every 5 years 50% of the software developers out there have less than 5 years experience (let that sink for a sec).

I’m giving this introduction myself since what I’m about to tell you here is nothing new and has been talked/written/discussed about for many years. Still it might be useful and relevant for you or your colleagues.


If you’re not that new to software development, You’ve probably come across the term “functional programming” and as a part of that heard that a Function is best when it’s “pure”.
There are a few characteristics for a pure function and one of them is “not having any side effects”.
What does it mean and why is that important?

First of all, let’s understand what “no side effects” means. If to quote it from Wikipedia:

The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).

To put in simpler words, a function should not access or alter anything outside of its scope, but then the question arises - why?

Our example

Say we have the following function - it should instruct to open a modal with a certain content.
It will output a string according to the argument it got but also depending on the application’s global state (not a React state, just "a" state). It will also alter the application’s global state too (nitpickers of the world, this is just an example, don’t be harsh ;)

function openModal(content) {
   let result = '';

   if (isMobile) {
       shouldOpenModal = false;
   } else {
       shouldOpenModal = true;
       result = content ? `My content is ${content}` : `No content mate`;
   }

   return result;
}
Enter fullscreen mode Exit fullscreen mode

Many would claim that this function has nothing wrong with it. They will say that the function is able to access the global state so why shouldn’t it? We don’t need to burden it with another argument which is implicitly “there”.
But this function, as written now, is very problematic and I let me explain why -

Reason #1 - Flexibility

Our function does what we want for it to do alright, but it is tightly coupled to a certain state. If we would like to determine the output or whether or not open the modal according to a different argument we simply cannot do that. This means that our function, in its current state, is not flexible at all.
You know what is flexible? Our product’s demands. We need to take into consideration, and as long as it is not involving major code-design shifts, that the requirements will change and our code needs to support it elegantly.

Reason #2 - Reusability

We are very pleased with our function and we identify the need for it in other parts of the application (and even other applications). What we will usually do is extract our function into a package we can distribute and reuse, alas - our function now has lost its “context” since the package has no idea what “isMobile” or “shouldOpenModal” is, which forces us to refactor it.
The more we rely on external state, it would be harder for us to “move” our function from its comfortable nest.

Reason #3 - Understandable Mutability

Here’s a mouthful for you ;)
What I mean by that is a situation where you mutate some out-of-the-function-scope data by reference, for example, the way we change the state like this shouldOpenModal = true;. Oh dear god.
This sort of code is the main reason for “... but what the hell changes that state?!” questions when production is down at 03:00am. When there are hidden “jams” like this in your functions the mutation flow is very hard to reason about and this is the main reason why you need to avoid it.
When your function reads from a state, it opens the door for developers to also “write” to it.

Reason #4 - Testability

You’d be surprised that many would claim that “testability” is in the lowest priority here (don’t know who you people are) but testability is one of the tools we have to ensure our code is well written so later on we can easily refactor and reason about it.
If you attempt to test the function example we have you need to supply the global variables isMobile and shouldOpenModal so that the function can access them. This means that your test’s code now has to create a defined “environment” which the function can run in. This surfaces the fact that the function cannot “live” outside this env which relates to reasons #1 and #2. A code that cannot run outside its environment is much harder to refactor.

Wrapping up

I hope the reasons above made it a bit clearer why function’s side effects are bad for you and why you need to avoid them. Feel free to use this resource as a reference whenever you or one of your colleagues offers to read from the state “since it’s there”. If you want to write our function example without the side effects, this is one option:

function openModal(content, shouldOpenModal) {
   let result = {};

   result.shouldOpenModal = shouldOpenModal;
   result.content = content ? `My content is ${content}` : `No content mate`;

   return result;
}
Enter fullscreen mode Exit fullscreen mode

The code line result.shouldOpenModal = shouldOpenModal; is pretty much redundant. We're calling a function called "openModal" where the outcome can be not opening the modal at all, which is a bit weird. Well, perhaps that's another post for naming and single responsibility, but this... this is just an example to make my point, remember? ;)

Not such a big change in this case, but you can go over the 4 reasons I presented above and see how this answers them all -

  • The “shouldOpenModal” argument can be derived from anything we see fit and not be tightly coupled to the isMobile global state
  • We can move this function to wherever we want to since it does not rely on any external data
  • We do not mutate any external state here. We return values which later can be used to mutate the global state by the function’s caller
  • We can easily test this function, passing the required arguments and checking the output. Our tests are reliable since nothing outside the function can change the output!

As always, if you have any comments or questions about what's written, be sure to leave them in the comments below :)

Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻

Photo by Joanna Nix-Walkup on Unsplash

Top comments (1)

Collapse
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati

Thanks for sharing it. I am gonna link to this article in my Python course.