A real program has to eventually save to a database, call an API, or print something — side effects can't actually be eliminated. What functional programming really asks for is pushing them to the edges, not pretending they don't exist.
Beginner
2 min read
Purity isn't the same as "never causes an effect"
The pure functions lesson earlier in this domain defined a pure function as
one whose output depends only on its input, with no observable side
effects. Taken literally and applied to an entire program, this sounds
impossible — a program that never reads input, writes output, or touches a
database isn't a useful program, it's a calculator nobody can see the
result of.
The resolution: purity is a property of individual functions, not a
requirement that an entire program somehow avoid touching the outside world
at all. The actual functional-programming discipline is about where
effects happen, not whether they happen at all.
The pattern: pure core, effects at the edges
// Pure: computes a value, no I/O, easy to test with plain assertionsfunction calculateInvoiceTotal(items, taxRate) { const subtotal = items.reduce((sum, item) => sum + item.price, 0); return subtotal * (1 + taxRate);}// Impure: the actual I/O, kept separate and thinasync function processOrder(order) { const total = calculateInvoiceTotal(order.items, TAX_RATE); // pure logic await saveToDatabase(order, total); // effect await sendConfirmationEmail(order.customerEmail, total); // effect}
calculateInvoiceTotal is pure — same inputs, same output, every time, with
nothing to mock in a test. processOrder is impure by necessity (it has to
actually save data and send an email), but it stays thin: it calls the pure
function for the actual computation and only wraps a small, explicit layer
of unavoidable effects around it.
Why this separation is worth the discipline
Isolating effects at the edges means the bulk of a program's real logic —
the part with actual branching, calculations, and business rules — can be
tested with plain input/output assertions, no mocking a database or a
network call required. The thin, impure layer at the edges still needs
testing, but it's small and mostly just "does it call the right function
with the right arguments," which is a much easier thing to verify than the
logic itself.
Why this matters day to day
"Is this function pure?" is a genuinely useful design question to ask while
writing code, not just a theoretical classification after the fact —
noticing a calculation tangled together with a database call is a signal to
separate them, pulling the actual logic out into its own pure function and
leaving only the unavoidable I/O in the outer layer. This single habit —
pushing effects outward, keeping logic pure — is arguably the most
practically valuable idea in this entire domain, usable in any language,
functional or not.