You have a function called addToCart that does three things in the same place: it adds the item to an array, calls an API to persist the cart, and fires an analytics event. The unit test, if it exists, mocks all three things. You change one and break the other two without noticing. That's the problem functional programming solves, not with abstract math, but by separating which part of your code does something (side effect) from which part just calculates something (transforms data).
Actions, calculations, and data: the distinction that matters
An action is any function whose result depends on when or how many times it runs, or that touches something outside itself. saveUser(user) that does a POST to your API is an action: calling it twice creates two records, calling it without network access fails. Date.now() is also an action, even though it doesn't mutate anything: the result depends on the exact moment it runs.
A calculation is a pure function: same input, same output, touching nothing outside itself. const total = items => items.reduce((sum, item) => sum + item.price * item.qty, 0) is a calculation. It doesn't matter if you call it once or a thousand times, in a test or in production: for the same items array, the result is always the same.
Data is the facts, with no behavior: { id: 'sku-104', price: 25000, qty: 2 }. In JavaScript it's tempting to attach behavior to it with classes and methods, but the more you treat it as an inert structure, the easier it is to reason about, serialize, and compare in a test with a simple toEqual.
Refactoring a real action
Let's go back to addToCart. Here's how it starts, mixing all three things: function addToCart(item) { cart.push(item); localStorage.setItem('cart', JSON.stringify(cart)); analytics.track('item_added', { sku: item.id }); }. This function can't be tested without mocking localStorage and analytics, and it can't be reused in a context where you don't want to persist anything yet, for example when preloading a cart from the server.
The first step is separating the calculation from the rest: what the new cart is, not how it gets persisted or what gets tracked. const addItem = (cart, item) => [...cart, item]. This version is a pure calculation: it takes the current cart and the item, returns a new cart, and touches nothing external. You can call it a thousand times in a test without setting up a single mock.
The actions stay outside, explicit, in the orchestration layer: function handleAddToCart(item) { cart = addItem(cart, item); persistCart(cart); analytics.track('item_added', { sku: item.id }); }. persistCart and analytics.track are still actions, that doesn't change, side effects exist because software has to do things in the real world. What changed is that the calculation of the cart's new state is isolated, testable without mocks, and reusable anywhere you need the same logic without dragging the effects along.
Where the difference shows up in a real system
This separation matters more as the system grows. A checkout pipeline with stock validation, tax calculation, discount application, and shipping calculation becomes manageable when each of those steps is a pure calculation you chain together, and the actions (charging the card, updating inventory, sending the confirmation email) sit at the edges, running once at the end, in the right order. If you mix everything together, every change to the discount logic forces you to check whether you broke the card charge, because they're in the same function.
The same criterion applies on a backend with complex business logic, in data transformation for an analytics pipeline, or in frontend state with Redux or any store: if you can move the logic of what should happen out of how it executes and with what effects, the resulting code is easier to test without mocks, easier to parallelize without race conditions, and easier to read because each function does one thing that fits in a single sentence.
Functional programming doesn't force you to use Haskell or avoid classes altogether. The part that actually changes you as a senior developer is simpler and more practical: before writing a function, ask yourself whether you're calculating something or doing something, and if it's the latter, ask whether it really needs to be mixed with the calculation. Most of the time, it doesn't.




