The accumulator pattern — building a result recursively without mutation
The recursion lesson earlier in this domain showed recursion replacing loops. This lesson closes out the domain with the specific technique that makes recursion practical for building up a result — an extra parameter that carries the answer along instead of mutating one.
Beginner
3 min read
The problem: recursion without an obvious place to build a result
A first attempt at summing a list recursively often looks like this:
function sum(list) { if (list.length === 0) return 0; return list[0] + sum(list.slice(1));}
This works, but notice it doesn't build the result incrementally the way an
imperative loop with a running total would — it waits for every recursive
call to finish before any addition happens, and each call has to hold onto
its own pending + until the whole chain resolves. For sums this is fine;
for problems where you genuinely want to carry a growing result forward,
this shape doesn't fit naturally.
The fix: pass the running result along as an extra parameter
The accumulator is an extra parameter that carries the answer-so-far
forward through each recursive call, instead of combining results on the
way back up the call stack. By the time the base case (list.length === 0)
is reached, the accumulator already is the final answer — it just gets
returned directly, with no further combining work left to do.
Why this specific shape matters: tail calls
This restructured version is also exactly what makes tail call
optimization possible, the technique the earlier recursion lesson
mentioned as a way some languages avoid stack overflow on deep recursion.
The original version (list[0] + sum(...)) has to keep a pending addition
around after the recursive call returns — it's not a tail call. The
accumulator version's recursive call is the very last thing that happens,
with nothing left to do afterward — a genuine tail call, which a supporting
language's compiler can optimize into a loop internally, reusing one stack
frame instead of growing one per call.
One honest caveat, from the earlier recursion lesson: being able to be
optimized isn't the same as being optimized. Scheme, Erlang, and Elixir
guarantee tail-call optimization, but JavaScript engines other than Safari's
don't implement it, so the JavaScript sum above still overflows the stack
on a very long list. The accumulator shape is the prerequisite for the
optimization, not a promise that your runtime performs it.
The general pattern, beyond summing
The same shape — an accumulator parameter, updated and passed forward,
returned once a base case is reached — extends directly to building lists,
not just numbers:
function reverse(list, accumulator = []) { if (list.length === 0) return accumulator; return reverse(list.slice(1), [list[0], ...accumulator]);}
Why this matters day to day
Recognizing "this recursive function isn't tail-recursive because it still
has work to do after the recursive call returns" — and knowing the
accumulator pattern is the standard fix — closes out this domain's
recursion coverage with the specific, practical technique for building up a
result recursively — and the prerequisite for tail-call optimization in the
languages that provide it.