Understanding Side Effects in JavaScript

Posted by

What they are, why they matter, and how to manage them for clean, predictable code.

What they are, why they matter, and how to manage them for clean, predictable code.

Introduction

Every JavaScript app has side effects. You can’t build anything useful without them. Updating the DOM, fetching data, writing to a database, logging errors — all are side effects.

But here’s the kicker: uncontrolled side effects make your code unpredictable, hard to test, and full of hidden bugs. Controlled side effects make your code reliable, testable, and easier to maintain.

In this article, we’ll demystify side effects in JS with practical examples, real-world scenarios (frontend + Node.js), common pitfalls, and strategies for keeping them under control.


1. What Is a Side Effect?

A side effect is any observable change outside of a function’s scope.

👉 If a function changes something beyond returning a value, that’s a side effect.

Examples:

  • Modifying a global variable.
  • Writing to the DOM.
  • Logging to the console.
  • Fetching data from an API.
  • Writing a file.
  • Generating random numbers.
  • Using the current date/time.

Pure vs Impure Functions Recap

  • Pure: No side effects, same input → same output.
  • Impure: Causes side effects or depends on external state.

2. Side Effects in the Browser

DOM Manipulation

function addItem(text) {
const li = document.createElement("li");
li.textContent = text;
document.querySelector("ul").appendChild(li); // side effect
}

Event Handling

button.addEventListener("click", () => {
alert("Clicked!"); // side effect (UI, global alert box)
});

Local Storage

function saveToken(token) {
localStorage.setItem("auth_token", token); // side effect
}

3. Side Effects in Node.js

File I/O

const fs = require("fs");

function saveData(data) {
fs.writeFileSync("data.json", JSON.stringify(data)); // side effect
}

Database Queries

async function getUser(id) {
return db.findById(id); // side effect: DB query
}

Logging

function log(message) {
console.log(message); // side effect: writes to stdout
}

4. Why Side Effects Are Necessary

Without side effects, your app can’t:

  • Render UI.
  • Fetch or save data.
  • Communicate with APIs.
  • Provide real interactivity.

👉 The goal isn’t to eliminate side effects. It’s to control and isolate them.


5. Why Side Effects Are Dangerous

  • Hard to test: You need mocks/spies.
  • Unpredictable: Depends on external state (e.g., Date.now()).
  • Hidden mutations: Mutating objects/arrays in place breaks expectations.
  • Race conditions: When multiple async side effects interact unpredictably.

Example bug:

let cart = [];

function addToCart(item) {
cart.push(item); // mutates external state
}

Calling addToCart changes global state—hard to predict in large apps.


6. Common Side Effect Pitfalls

a) Mutating Arguments

function addItem(arr, item) {
arr.push(item); // ❌ mutates input
return arr;
}

Fix (pure):

function addItem(arr, item) {
return [...arr, item];
}

b) Using Randomness or Time

function getOrderId() {
return Math.random(); // ❌ impure, unpredictable
}

Fix: Inject randomness/time as dependency for testability.

function getOrderId(randomFn = Math.random) {
return randomFn();
}

c) Hidden Async Effects

function fetchData() {
let result;
fetch("/api/data").then(r => result = r.json()); // ❌ async side effect not exposed
return result; // undefined
}

Fix: Return the promise.

function fetchData() {
return fetch("/api/data").then(r => r.json());
}

7. Managing Side Effects in Practice

Strategy 1: Functional Core, Imperative Shell

  • Core = pure functions (calculations, validation, transforms).
  • Shell = impure functions (I/O, DOM, API calls).
// Pure
function calculateTotal(cart) {
return cart.reduce((sum, i) => sum + i.price * i.qty, 0);
}

// Impure
function checkout(cart) {
const total = calculateTotal(cart);
console.log("Charging $" + total); // side effect
}

Strategy 2: Isolate Side Effects

Wrap side effects so they’re easy to swap/mocks in tests.

function apiFetch(url, options) {
return fetch(url, options);
}

// usage
const data = await apiFetch("/api/user");

Now you can replace apiFetch with a mock in tests.


Strategy 3: Declarative Frameworks

React, Redux, Vue — designed to reduce manual side effects.

  • React components are mostly pure (render output based on props).
  • Side effects live in useEffect or actions.
useEffect(() => {
fetch("/api/data").then(setData); // isolated side effect
}, []);

Strategy 4: Async Patterns

  • Use Promise chains or async/await to structure async side effects clearly.
  • Centralize error handling with .catch or try/catch.
  • Limit concurrency with Promise.allSettled, Promise.any, or custom pools.

8. Testing Side Effects

Pure Function

test("calculateTotal", () => {
expect(calculateTotal([{ price: 10, qty: 2 }])).toBe(20);
});

Easy — no mocks.

Impure Function

test("logs checkout", () => {
const spy = jest.spyOn(console, "log").mockImplementation(() => {});
checkout([{ price: 10, qty: 2 }]);
expect(spy).toHaveBeenCalledWith("Charging $20");
spy.mockRestore();
});

Requires mocks/spies.


9. Async Side Effects: Real-World Examples

React State

setCount(count + 1); // side effect: schedules a re-render

API Request with Timeout

const withTimeout = (p, ms) =>
new Promise((res, rej) => {
const t = setTimeout(() => rej(new Error("Timeout")), ms);
p.then(v => { clearTimeout(t); res(v); },
e => { clearTimeout(t); rej(e); });
});

await withTimeout(fetch("/api/data"), 5000);

Node Server Request

http.createServer((req, res) => {
res.writeHead(200);
res.end("Hello World"); // side effect
}).listen(3000);

10. Quick Reference: Side Effect Do’s and Don’ts


Conclusion

Side effects are unavoidable — but they don’t have to be unmanageable. The key is to keep core logic pure and isolate side effects at the edges. That way:

  • Your logic stays predictable.
  • Your tests stay simple.
  • Your side effects are easier to control, monitor, and debug.

Pro tip: Next time you write a function, ask: Does this only compute and return a value, or does it also change something outside? That’s how you spot side effects — and decide where they belong.


Call to Action

What’s the trickiest side effect you’ve wrestled with — stale closures, async API calls, or hidden DOM mutations?

💬 Share your story in the comments.
🔖 Bookmark this as your side-effect survival guide.
👩‍💻 Share with a teammate who thinks console.log isn’t a side effect.

Leave a Reply

Your email address will not be published. Required fields are marked *