A function meant to compute a total also writes a line to a log, updates a global counter and sends a metric to an analytics service. Its name says none of that.
Those extra actions have a name: side effects. They are neither forbidden nor suspicious, but they change how a function is tested and read.
Definition
A side effect is any action of a function observable from the outside, beyond the value it returns. Modifying a shared variable, writing to browser storage, printing something, calling a service: all of it counts.
let log = [];
function pureSum(numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
function sumWithEffect(numbers) {
log.push("summed " + numbers.length + " values"); // side effect
return pureSum(numbers);
}
console.log(sumWithEffect([1, 2, 3])); // 6
console.log(log); // [ 'summed 3 values' ]Both functions return the same value. The second one also leaves a trace in the program, and that trace is invisible to anyone reading only its call.
The most common forms
- Modifying a received argument: a Mutation the caller never asked for. The most insidious of the four.
- Writing an outside variable: a global counter, a cache, a state flag.
- Touching the outside world: localStorage, a fetch() call, a line printed to the console.
- Throwing an error: a throw diverts the program flow, which is an effect too.
The problem is not the effect, it is its scattering
A program with no effects is useless: it shows nothing and records nothing. What costs is an effect hidden in the middle of a function that looks like it only computes.
The practice is to gather the effects in one place, usually the edge of the application, and keep the rest as Pure function. Concretely, one function computes and returns, another takes that result and writes it somewhere.
A simple signal for spotting them: if the function name starts with a computing verb but its body contains a write, there are two responsibilities inside one function.
Frequently asked questions
Is a console.log() really a side effect?
Technically yes, since it writes outside the function. In practice it is harmless: it changes no data and disappears without consequence. The question is only worth asking when console output becomes a logging mechanism other systems rely on.
How do I test a function that produces one?
By replacing the dependency with a controlled double, a mock, and checking that it was indeed called. It is doable but always longer than testing a pure function. The best saving is often to extract the computation into a separate function, tested directly.
Does Array.forEach() necessarily produce an effect?
Almost always, yes, since it returns nothing: its only point is to do something on each pass. When you want to produce a value rather than act, Array.map() or Array.reduce() express the intent better and leave the original data alone.