Pure functions and side effects in Javascript

Anton Ioffe - September 7th 2023 - 15 minutes read

In this evolving era of modern web development, understanding core JavaScript concepts like pure functions and side effects is key. They lie at the crossroads of functional programming, asynchronous operations, and development frameworks like React. But, how do they work, and why do they matter? This article will dissect these topics, bringing you a step closer to mastering JavaScript's advanced aspects and effectively using them in your coding journey.

We will kick things off by diving deep into the intriguing world of pure functions. We will take you through their deterministic nature, their predictability, and how they work with local variables and recursion. As we contrast pure functions with their not-so-pure counterparts, you'll grasp how functions like map and push stand amid the pure vs. impure debate.

Then, we set ourselves off on a quest to explore the realm of side effects in JavaScript – what they are, how they impact functionality, and if they're always problematic. Not just that, we will also share some strategies on how to minimise side effects through functional programming while leveraging the power of pure functions. The culmination of this will be to understand how all this fits into modern development landscapes, particularly asynchronous programming and React frameworks. Intrigued much? Let’s pull the curtain on this JavaScript saga and get started!

Understanding Pure Functions in JavaScript

Before delving deep into the concept of pure functions in JavaScript, it's important to understand the relevance of these within the scope of modern web development and functional programming.

A Pure Function in JavaScript is a function where the return value is determined exclusively by its input values, without any observable side effects. The principal characteristics of pure functions can be summarized as:

  1. Deterministic: Pure functions always produce the same output given the same input. This predictability of functions makes code easier to debug and test, as the function's behavior is consistent across separate executions.

  2. No Side Effects: Pure functions do not interact with or modify variables or states outside their scope, making them stateless operations. Any dependencies are passed as arguments. If a external state does exist, it can lead to unpredictable behavior and bugs, which are quintessential examples of side effects in JavaScript.

To better illustrate these properties, consider the following simple JavaScript code examples.

Here's an example of an impure function:

let count = 1;

function increment() {
    count += 1;
    return count;
}

console.log(increment()); // Outputs 2
console.log(increment()); // Outputs 3

The increment() function above is not a pure function because it modifies a global variable count. This leads to different output, even when the same function is called multiple times, making our code unpredictable.

However, with a minor modification we can transform this into a pure function:

function increment(n) {
    return n + 1;
}

console.log(increment(1)); // Outputs 2
console.log(increment(1)); // Outputs 2

In the pure function example provided, increment() purely depends on the input value and does not produce any side effects, hence it does not modify any external state.

More complex scenarios can also benefit from pure functions. These allow for function composition, use of local variables, recursion and embedding other pure functions within them.

Developers often inadvertently write impure functions, such as using the 'push' method in JavaScript to add an item to an array. Consider the following:

let numbers = [1, 2, 3];

function addNumber(arr, num) {
    arr.push(num);
    return arr;
}

console.log(addNumber(numbers, 4)); // Outputs [1, 2, 3, 4]
console.log(numbers); // Outputs [1, 2, 3, 4]

In this case, the addNumber() function is not pure, as it modifies the original numbers array.

To maintain function purity, a different approach such as using the spread operator can be utilised:

let numbers = [1, 2, 3];

function addNumber(arr, num) {
    return [...arr, num];
}

console.log(addNumber(numbers, 4)); // Outputs [1, 2, 3, 4]
console.log(numbers); // Outputs [1, 2, 3]

In the addNumber() function here, the return value uses the spread operator to concisely add a new number to the original array without modifying it, thus avoiding side effects and maintaining function purity.

Implementing pure functions in your JavaScript code might seem demanding but they play a crucial role in modern development frameworks like React, as functional component behavior is modeled around similar principles. The goal of leveraging pure functions in any language, including JavaScript, is to promote code that is easier to reason about, testable, and more reliable, improving the overall quality of your software.

Keep in mind though, that sometimes tuning every function to be pure might lead to a trade-off between readability and complexity, especially when recursively combining functions, oversimplifying complex operations or having more arguments than needed to avoid side effects. An important consideration is that operations like logging and error handling could be near impossible without side effects.

As a final thought-provoking point, consider some of the common impure functions you've recently written or utilized in your codebase. How could you convert these into pure functions? What side effects do they have that could be eliminated or handled differently to achieve function purity? What are some cases where the function's impurity is necessary and justifiable? Reflecting on these specific questions might lead you to a deeper understanding of function purity and its practical implications in your coding journey.

Pure Functions vs. Impure Functions: Contrasting Approaches

In the realm of JavaScript, much thought is given to the concepts of pure and impure functions. They represent two contrasting ways you can handle and manipulate data within your codebase, each with its separate pros and cons.

Let's start with the basics. A pure function is a function whose output is solely determined by its input and has no side effects. A side effect is any effect, other than the return value, that alters the external state of the software. On the other hand, an impure function is a function that, apart from returning a value, modifies some state or has an observable interplay with its calling functions or the outside world.

Pure Functions in Javascript

A pure function, as stated prior, will always produce the same output given the same input and it has no side effects. Let's illustrate this with a simple example:

function add(a, b){
    return a + b;
}

In this add function, given the same input parameters a and b, the function will always return the same result without altering the input parameters or any other data in the external environment. This makes code with pure functions easier to debug and test because you can anticipate the output just from knowing the input.

Impure Functions in Javascript

In contrast, impure functions may produce side effects and their output may rely on the external state. Here's an example of an impure function:

let x = 1;

function addY(y){
    x = x + y;
    return x;
}

In this addY function, the result not only depends on the input but also on the global variable x. Moreover, it alters the global variable x which is a side effect.

Understanding map and push

Array methods in JavaScript provide examples of pure and impure functions. Take Array.prototype.map and Array.prototype.push. The map method is pure as it returns a new array and does not modify the original array. In contrast, the push method changes the original array, thus it's an impure function. Here is a simple demonstration:

// The .map() method

const originalArray = [1, 2, 3];
const newArray = originalArray.map(num => num * 2);

console.log(originalArray); // [1, 2, 3]
console.log(newArray); // [2, 4, 6]
// The .push() method

const originalArray = [1, 2, 3];
originalArray.push(4);

console.log(originalArray); // [1, 2, 3, 4]

Common Mistakes

A frequent mistake when handling pure functions is assuming that all JavaScript methods are pure. As displayed in the push example, not all methods are pure and care should be taken when choosing methods in a functional programming style.

A common approach to avoid impurity and side effects is to make a copy of the original data before modification. The spread operator (...) can be quite useful for this:

const originalArray = [1, 2, 3];
const newArray = [...originalArray];
newArray.push(4);

console.log(originalArray); // [1, 2, 3]
console.log(newArray); // [1, 2, 3, 4]

In this revised example, despite using the impure push method, we managed to retain the purity of our original data by first creating a copy of the original array before the modification.

It's important to keep in mind that pure functions are not inherently better or worse than impure ones. Each has its own use cases and they can often be used together in a single codebase to produce efficient, readable, and scalable code. The key is understanding their differences and knowing when to use which.

Exploring Side Effects in JavaScript

Understanding the concept of side effects in JavaScript is crucial for clean and efficient programming. A side effect refers to any observable change in a system's state outside of returning a value from a function. These changes might include alterations to global or mutable variables, database manipulations, network requests, or even timestamp logs that stem from a function's calculations.

To illustrate the concept, let's delve into a JavaScript code example showcasing side effects.

Side Effects: Tinkering with Global Variables

// Declaration and initiation of our global variable
let counter = 0; 

function incrementCounter(){
    // This line amends the global variable, resulting to a side effect
    counter++; 
}

// Function invocation
incrementCounter(); 
console.log(counter); // Outputs: 1

In the preceding example, the function incrementCounter() alters the global variable counter, representing a side effect. This change proliferates beyond the function, leaving its impact globally. Such alterations are problematic, especially in large applications, as the use of global variables could breed many bugs and inconsistencies, making debugging a daunting task.

As your proficiency in JavaScript increases, you'll discover that side effects are commonplace. They are integral parts of functions involving network requests, logging, database operations, etc. While often unavoidable and sometimes vital, these side effects come with their own set of challenges.

The Downsides of Side Effects

Side effects muddle up your code, making it harder to read, debug, and maintain. They inject an element of unpredictability into your code, making it behave much like a well-seasoned dish - a little spice enhances the flavor, but too much ruins it all!

The complexity escalates when dealing with situations involving shared global states or concurrent programming. These scenarios augment the labyrinth your code needs to navigate.

Shared Mutable State: A Perilous Voyage

// Shared variable
let userName = 'Tom'; 

function hello(){
    // This line references the shared variable
    return `Hello, ${userName}`; 
}

function changeName(newName){
    // This line alters the shared variable
    userName = newName; 
}

console.log(hello()); // Outputs: 'Hello, Tom'
changeName('Alice');
console.log(hello()); // Outputs: 'Hello, Alice'

As our above example underscores, grappling with shared mutable states coupled with side effects, paves the way for unpredictability and complexity. Today's 'Tom' could easily transform into tomorrow's 'Alice' with no direct notifications, leading to code mismanagement and chaos.

Pure Functions: Your Way to Clarity

To make your code predictable and easy to understand, using pure functions is an effective approach. These functions act independently, their output solely dictated by their input. Moreover, they neither rely on nor change any external states.

Let's transform our hello() function into a pure one.

function hello(name){
    // Welcome, Pure Function!
    return `Hello, ${name}`; 
}

// Greetings from Pure Functions land
console.log(hello('Alice')); // Outputs: 'Hello, Alice'

Now, our hello(name) function is independent of any external influences and predictable. Its output depends only on its input, namely name. Let's also revamp changeName() along the same lines:

function changeName(newName){
    // This function now just returns the newName without mutating any external states
    return newName;
}

console.log(hello('Tom')); // Outputs: 'Hello, Tom'
let name = changeName('Alice');
console.log(hello(name)); // Outputs: 'Hello, Alice'

Now, neither hello(name) nor changeName(newName) interact with any external states. They operate in a predictable manner since their output is solely dependent on their input.

It's important to remember that while side effects might be inherent in the coding process, they don't have to rule your code. You can use pure functions to bolster predictability, leading to more manageable code.

Reflect and Re-evaluate

As you wrap up this section and gear up for the next, consider these questions:

  • "How can minimizing side effects in my functions increase the predictability of my code?"
  • "What measures can I take to improve the management of external dependencies in my codebase?"
  • "How can I leverage pure functions to make my code cleaner, simpler, and more easily understandable?"
  • "Can I recall an instance where transitioning to a pure function could have enhanced the functionality or readability of my code?"

Reflect on these inquiries and re-assess your coding practices. Remember that the end-goal is to create efficient, maintainable, and clean code. Happy coding!

Minimizing Side Effects and Leveraging Pure Functions in JavaScript

In leveraging the strength of pure functions and managing side effects in JavaScript, it's key to deeply understand functional programming concepts, evaluate the impact of state manipulation, and strategically place side effects in your code. With the right approach, these concepts will play an integral part in a developer's everyday coding routine.

Recognizing Pure Functions in JavaScript

To kick things off, it's important to understand what a pure function is. A pure function is one whose return value is dictated solely by its input values, without bringing about any observable side effects. This concept, which borrows from the mathematical definition of a function, can be seen in basic arithmetic operations as well as in tasks like measuring the length of a string.

function add(a, b) {
    return a + b;
}

add(2, 3); // returns 5. It's purely based on the input and has no side effects.

function stringLength(str) {
    return str.length;
}

stringLength('Hello'); // returns 5. No side effects, purely depends on input string.

Incorporating Pure Functions in Your Code

Armed with the knowledge of what pure functions are, the next step involves incorporating them into your regular coding tasks. Pure functions present a plethora of benefits. They are simple, intuitive, predictable, and inherently more stable than impure functions. Code primarily composed of pure functions tends to be resilient, less susceptible to disguised bugs, and manageable.

Even though JavaScript is a multi-paradigm language that supports object-oriented constructs, maintaining the purity of functions is crucial. Here's how you can use pure functions while calculating areas:

function calculateArea(radius) {
    return Math.PI * radius * radius;
}

calculateArea(10); // returns 314.1592653589793. It's a pure function as the output solely depends on its radius input.

Managing Side Effects in JavaScript

Side effects surface in a function when it alters a state outside its own scope, such as by modifying a global object or mutating the value of its parameters. While building entirely side effect-free code is often impractical, controlling where and when these side effects occur becomes a key goal.

An often made mistake is to modify what appears to be a global object within a function, thereby generating a side effect. This may lead to unexpected behaviors and is a common root cause of hard-to-spot bugs in a codebase. Here's an example demonstrating this mistake:

let circleData = {};

function calculateAndStoreArea(circle) {
    let area = Math.PI * circle.radius * circle.radius;
    circleData.area = area;
    return area;
}

calculateAndStoreArea(new Circle(10));

An improved way to handle this is shown below:

function calculateArea(circle) {
    return Math.PI * circle.radius * circle.radius;
}

const circleData = { area: calculateArea(new Circle(10)) };

By doing this, the side effect present in the earlier version—modifying the circleData object—is isolated to the circleData object assignment. Notably, calculateArea remains as a pure function as it was before.

Benefits of Pure Functions

Pure functions bring along a host of advantages. They are inherently predictable because their output is solely contingent on their input, making debugging and testing much simpler. They result in cleaner, less complicated code, easing maintenance and streamlining future refactoring efforts. Furthermore, their lack of dependence on state makes them ideally suited to parallelizable applications, aiding in mitigating common threading issues. As developers enhance their understanding of these principles, they're able to better control side effects within their ongoing projects. The ultimate aim isn’t to completely eradicate side effects, but to effectively manage and control them.

Pure Functions and Side Effects: Impact on Modern Development and Asynchronous Operations

The advent of modern frameworks such as React and the rise of asynchronous operations in JavaScript development create new environments where we can examine the application of pure functions and side effects. Interestingly, while these concepts seem simplistic on the surface, their implications for code quality, modularity, performance, and more are extensive.

Pure Functions in React and Asynchronous Programming

Being one of the most widespread JavaScript libraries, React indeed incorporates the idea of pure functions in their model. React components can be constructed as pure functions that accept parameters, known as 'props', and return a React element. These components, often referred to as stateless functional components, have several advantages:

Pros:

  1. They lead to more predictable code since their output solely relies on the input.
  2. They are easier to test and maintain because of their simplicity.
  3. They potentially have performance benefits due to the absence of lifecycle methods and state.
function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}

Nevertheless, this does not mean all components should be pure. Stateful components are indispensable for handling complex UIs, as the stateless components are on their own limited:

Cons:

  1. They always re-render when a parent component re-renders, leading to unnecessary rendering and potentially performance issues.
  2. They lack lifecycle methods, making manipulating DOM directly or fetching data on component rendering not possible.
  3. They cannot use refs for managing focus, text selection, or media playback.

As for asynchronous operations, converting async functions to pure ones imposes a specific constraint: A pure function runs synchronously and returns a value. Conversely, an async function returns a promise, which eventually resolves into a value. This introduces an element of unpredictability. Hence, pure functions’ principles and async operations seem to contradict.

//standard async function
async function fetchData(url){
    let response = await fetch(url); 
    let data = await response.json();
    return data;
}

Common Mistakes and Their Fixes

Common mistakes while writing pure functions might be introducing side effects, such as modifying an external state, which defeat the purpose of using them.

Mistake: Modifying an external variable.

let name = 'John';
function setName(newName){
    name = newName;   //modifying external state
}

Solution: Avoid altering any values outside the function.

let name = 'John';
function getName(){
    return name;   //not modifying external state
}

For async functions, a common mistake would involve directly returning a response instead of the promise.

Mistake: Returning response from an async function

async function fetchData() {
    let response = await fetch(url);
    return response.json();  //Incorrect
}

Solution: Always return a promise from async functions

async function fetchData() {
    let response = await fetch(url);
    return response;  //Correct
}

The Big Question

Can pure functions modify external states? The straightforward answer is no. Pure functions should always return the same value given the same arguments and should cause no side effects, making them unable to alter external states. On the other hand, in functional programming languages and specific JavaScript cases, it is possible to preserve function purity even when the function seems to modify a state. It achieves this by ensuring that it doesn't mutate an existing object, but instead, generates and returns a new object that represents the new state.

While theoretical aspects of these principles might seem clear, their effective implementation can be full of subtleties. The contraposition between pure functions and side effects continues to shape JavaScript's evolution and how developers write efficient, maintainable code. It surfaces intriguing questions: How do the benefits of pure functions influence the trade-offs between performance and code readability? How do we effectively manage side effects with pure functions in dynamic environments? Where is JavaScript heading in terms of these fundamental paradigms? The more we dig, the more we unveil.

Summary

Pure functions and side effects are fundamental concepts in JavaScript and modern programming. Pure functions are those that return values solely based on their input without causing any side effects, such as altering a global variable or state. This makes them predictable and easy to test. On the other hand, side effects refer to any changes outside of the function's scope and can introduce unpredictability and complexity.

Modern frameworks like React incorporate the concept of pure functions, particularly in stateless functional components, leading to more predictable and potentially efficient code. However, with the rise of asynchronous operations, there's a seeming contradiction between the principles of pure functions and asynchronous programming. While pure functions should not modify external states, functional programming can allow a semblance of this by returning new objects that represent the new state without mutating the original. The understanding and effective application of these principles is crucial in writing efficient, scalable, and maintainable code in today's dynamic coding environment.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers