The Quest Begins (The "Why")
Honestly, I spent an entire afternoon staring at a piece of code that felt like I was trying to solve a Rubik’s cube blindfolded. The task? Walk through a nested JSON object and flatten every key into a dot‑notation path — think turning {a:{b:{c:1}}} into {“a.b.c”:1}. My first instinct was to grab a while‑loop, push/pop a stack, and manually keep track of where I was. It worked… until the data got deeper than a Dungeons & Dragons campaign. Suddenly my iterator was juggling three different stacks, a couple of flags, and a feeling that I was missing something elegant.
I remember thinking, “There has to be a simpler way.” That’s when the little voice in my head (the one that sounds like Yoda when he’s had too much coffee) whispered: “When the problem looks like a smaller version of itself, recursion is the answer.” I laughed it off at first — recursion felt like that one boss level in Dark Souls where you keep dying because you’re overcomplicating the dodge. But curiosity won, and I dove in.
The Revelation (The Insight)
Here’s the breakthrough: recursion shines when the problem can be defined in terms of itself. Not just “it calls a function again,” but when you can legitimately say, “To solve X for size n, I first solve X for size n‑1 (or a smaller piece) and then combine the results.” Iteration, on the other hand, is the go‑to when you’re walking a linear list, counting items, or performing a fixed‑step transformation where the state doesn’t naturally split into sub‑problems.
The mental framework I now use is a quick two‑question litmus test:
- Does the solution for the whole input rely on solving the same problem on a strictly smaller piece? If yes → recursion is a natural fit.
- Will the recursive depth stay reasonably bounded (think logarithmic or linear in the size of the input, not exponential)? If yes → go ahead; if you’re staring at a potential stack overflow, consider an explicit stack or iteration instead.
When I applied this to the JSON flattener, the answer was clear: to flatten an object, I needed to flatten each of its properties (which are themselves objects or primitives) and then prefix the keys. The “smaller piece” was each nested property. The depth? At most the nesting level of the JSON — usually far below the call‑stack limit in JavaScript/TypeScript.
That “aha!” moment felt like finally seeing the hidden pattern in a Magic Eye poster: once you spot it, the whole picture snaps into focus.
Wielding the Power (Code & Examples)
The Struggle – Iterative Attempt
function flattenIterative(obj) {
const result = {};
const stack = [{ prefix: '', value: obj }];
while (stack.length) {
const { prefix, value } = stack.pop();
for (const [k, v] of Object.entries(value)) {
const newKey = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object' && !Array.isArray(v)) {
stack.push({ prefix: newKey, value: v });
} else {
result[newKey] = v;
}
}
}
return result;
}
It works, but look at all that bookkeeping: a manual stack, prefix concatenation, and a loop that’s easy to mis‑read when you’re tired.
The Victory – Recursive Solution
function flattenRecursive(obj, prefix = '') {
return Object.entries(obj).reduce((acc, [k, v]) => {
const newKey = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object' && !Array.isArray(v)) {
// Recurse on the nested object
return { ...acc, ...flattenRecursive(v, newKey) };
}
// Primitive value – assign directly
return { ...acc, [newKey]: v };
}, {});
}
Why this feels like a spell:
- The function calls itself only when it encounters a nested object — exactly the “smaller piece.”
- The
prefixcarries the accumulated path, so we don’t need an external stack. - The base case is implicit: when
visn’t an object, we just return the key/value pair.
Common Traps (The “Boss Moves” to Avoid)
-
Forgetting the base case – If you recurse on every value without checking
typeof v === 'object', you’ll end up trying to callObject.entrieson a string or number, blowing up the call stack. -
Mutating the accumulator incorrectly – Using
acc[newKey] = v; return acc;inside the reducer works, but if you later spread the result ({ ...acc, ...sub }) you must ensure you’re not overwriting keys unintentionally. A quickconsole.logof intermediate states saves hours of head‑scratching. -
Ignoring arrays – The snippet above treats arrays as primitives (which is fine if you don’t need to flatten them). If you do want to dive into arrays, add
|| Array.isArray(v)to the condition and pass an index‑based prefix (${newKey}[${i}]).
Why This New Power Matters
Now that I’ve internalized this litmus test, I spot recursion opportunities everywhere: tree traversals, parsing nested configs, generating permutations, even solving Sudoku with backtracking. The payoff? Cleaner code, fewer mutable variables, and a mental model that matches the problem’s natural shape.
More importantly, I’ve stopped defaulting to iteration out of habit. I now ask, “Can I describe this as a solution to a smaller version of itself?” If the answer is yes, I reach for recursion — confident that the call stack will stay sane because I’ve already bounded the depth.
It’s like leveling up from a basic sword to a lightsaber: the same goal (cutting through problems), but with far more elegance and a satisfying hum when it works.
Your Turn
Grab a piece of code you’ve written recently that uses a while‑loop or a for‑loop to walk a hierarchy (maybe a file system, a comment thread, or a game map). Ask yourself the two questions above. If the answer leans toward recursion, refactor it and see how the readability changes. Drop your before/after snippets in the comments — I’d love to see your “aha!” moments!
Happy coding, and may your stacks stay short and your insights deep! 🚀












