Extract the Impurity, Keep the Function Pure
A technique for turning an impure procedure into a pure function plus a small side-effecting shell, without deleting the side effect itself.
Last time settled that purity comes in degrees. That still leaves a real question: when a function call lands on the low-confidence side, what do you actually do about it?
Some side effects can't be removed. A database write has to happen somewhere. So does appending something to the page.
But "has to happen somewhere" doesn't mean it has to happen buried inside a function that looks otherwise pure.
A Function Hiding Two Side Effects
Say you're rendering a cart item onto the page:
let nextId = 0;
function renderCartItem(item) {
const id = `cart-item-${nextId++}`;
const li = document.createElement('li');
li.id = id;
li.textContent = `${item.name} - $${item.price}`;
document.querySelector('#cart-list').appendChild(li);
}This has two side effects, and one of them is easy to miss.
nextId++mutates a variable outside the function.appendChildmutates the live page.
renderCartItem isn't a function. It's a procedure wearing a function's syntax, exactly the kind the first post in this series warned about.
Split It Instead of Living With It
The fix isn't to delete the side effects. The page still needs that list item appended. The counter still needs to advance.
The fix is to separate "compute something" from "affect the world," and only let the second part touch anything outside itself.
function buildCartItemElement(id, item) {
const li = document.createElement('li');
li.id = id;
li.textContent = `${item.name} - $${item.price}`;
return li;
}buildCartItemElement takes an id and an item, and returns an element. Nothing about it depends on anything outside its own parameters. Nothing it does is observable outside its own return value.
Calling it now looks like this:
const id = `cart-item-${nextId++}`;
const el = buildCartItemElement(id, item);
document.querySelector('#cart-list').appendChild(el);The side effects didn't disappear. They moved to the outer shell, the three lines around the pure function, instead of hiding inside it.
Why Moving It Is Still a Real Win
Nothing here reduced how much impure code exists. What changed is where it lives.
buildCartItemElement can now be called from a test, a preview, a different rendering target, anywhere, without touching the DOM or a shared counter. You can reason about it in complete isolation, the exact payoff the last few posts have been chasing.
The impure lines are still there, but now there are only three of them, sitting in plain sight at the call site instead of buried on line four of a function that looked trustworthy from the outside.
This Won't Always Be Possible
Sometimes a side effect is too tangled into a function's logic to cleanly pull apart, or the cost of restructuring isn't worth what you'd gain.
When extraction genuinely isn't practical, there's a second technique: leaving the impurity in place but shrinking how much of your program it can actually touch. That's next.
Keep reading