Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
Published on HivePostify by @scipio · Sun Sep 06 2026
Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
What will I learn - You will learn what recursion is, and the two ingredients every recursive function needs; - how the call stack drives function calls, and how recursion builds up on it; - what a stack overflow is, and the input sizes that cause one; - how to recurse over nested data structures like trees, where recursion truly shines; - when recursion is the right tool and when a plain loop is better.
Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Node.js (20+) distribution, or just a modern browser console; - Episodes 1-23 read, especially functions and scope.
Difficulty - Intermediate
Curriculum (of the Learn JS Series): - [Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It](https://hive.blog/hive-196387/@scipio/learn-js-series-1-what-is-javascript-why-it-runs-everywhere-and-how-to-run-it) - [Learn JS Series (#2) - Variables and Bindings](https://hive.blog/hive-196387/@scipio/learn-js-series-2-variables-and-bindings) - [Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint](https://hive.blog/hive-196387/@scipio/learn-js-series-3-the-primitive-types-number-string-boolean-null-undefined-symbol-bigint) - [Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting](https://hive.blog/hive-196387/@scipio/learn-js-series-4-operators-and-expressions-arithmetic-comparison-logical-and-short-circuiting) - [Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use](https://hive.blog/hive-196387/@scipio/learn-js-series-5-strings-template-literals-unicode-and-the-methods-you-actually-use) - [Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope](https://hive.blog/hive-196387/@scipio/learn-js-series-6-numbers-ieee-754-why-01-02-is-not-03-and-how-to-cope) - [Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression](https://hive.blog/hive-196387/@scipio/learn-js-series-7-control-flow-ifelse-switch-and-the-ternary-expression) - [Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which](https://hive.blog/hive-196387/@scipio/learn-js-series-8-loops-for-while-forof-forin-and-when-to-use-which) - [Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting](https://hive.blog/hive-196387/@scipio/learn-js-series-9-functions-declarations-parameters-return-values-and-hoisting) - [Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables](https://hive.blog/hive-196387/@scipio/learn-js-series-10-scope-and-the-temporal-dead-zone-how-javascript-finds-your-variables) - [Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods](https://hive.blog/hive-196387/@scipio/learn-js-series-11-arrays-the-workhorse-data-structure-and-its-core-methods) - [Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting](https://hive.blog/hive-196387/@scipio/learn-js-series-12-objects-key-value-data-dot-vs-bracket-access-and-nesting) - [Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly](https://hive.blog/hive-196387/@scipio/learn-js-series-13-truthiness-equality-and-coercion-vs-done-properly) - [Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator](https://hive.blog/hive-196387/@scipio/learn-js-series-14-mini-project-a-command-line-tip-calculator) - [Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions](https://hive.blog/hive-196387/@scipio/learn-js-series-15-first-class-functions-passing-returning-and-storing-functions) - [Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins](https://hive.blog/hive-196387/@scipio/learn-js-series-16-arrow-functions-vs-function-syntax-this-and-when-each-wins) - [Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript](https://hive.blog/hive-196387/@scipio/learn-js-series-17-closures-the-single-most-important-idea-in-javascript) - [Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions](https://hive.blog/hive-196387/@scipio/learn-js-series-18-higher-order-functions-functions-that-take-or-return-functions) - [Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)](https://hive.blog/hive-196387/@scipio/learn-js-series-19-callbacks-and-the-callback-pattern-before-we-reach-promises) - [Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures](https://hive.blog/hive-196387/@scipio/learn-js-series-20-default-rest-and-spread-flexible-function-signatures) - [Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way](https://hive.blog/hive-196387/@scipio/learn-js-series-21-destructuring-parameters-named-arguments-the-js-way) - [Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case](https://hive.blog/hive-196387/@scipio/learn-js-series-22-the-this-keyword-five-rules-that-explain-every-case) - [Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly](https://hive.blog/hive-196387/@scipio/learn-js-series-23-call-apply-and-bind-controlling-this-explicitly) - [Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows](https://hive.blog/hive-196387/@scipio/learn-js-series-24-recursion-base-cases-the-call-stack-and-stack-overflows) (this post)
Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
Solutions to Episode 23 Exercises
Exercise 1 - call and apply:
js function sayCity() { return this.city; } console.log(sayCity.call({ city: "amsterdam" })); // "amsterdam" console.log(sayCity.apply({ city: "berlin" })); // "berlin"
The insight: with no arguments to pass, call and apply look identical; the difference only appears when you pass arguments (list versus array).
Exercise 2 - partial application with bind:
js function multiply(a, b, c) { return a b c; } const times12 = multiply.bind(null, 3, 4); // a=3, b=4 fixed console.log(times12(2)); // 24
The insight: null is passed as this because multiply does not use it; the following arguments pre-fill a and b.
Exercise 3 - a bound logger:
js const logger = { prefix: "[LOG]", write(msg) { return ${this.prefix} ${msg}; }, }; const boundWrite = logger.write.bind(logger); setTimeout(() => console.log(boundWrite("started")), 10); // "[LOG] started"
The insight: binding locks this to logger, so the detached function keeps the right prefix; unbound, this would be undefined.
With this finally nailed down over the last two episodes, we change gears completely. Today is about a single idea that trips up almost every beginner and then, once it clicks, becomes one of the most satisfying tools you own: functions that call themselves. Recursion.
What recursion is
Recursion is when a function calls itself to solve a problem by breaking it into smaller versions of the same problem. It sounds circular, and it would be an infinite loop, except for one crucial ingredient: every recursive function needs a base case, a condition where it stops calling itself and just returns an answer. Without a base case, it recurses forever (until it crashes, which we will see happen shortly).
So every recursive function has two parts, and I want you to memorize this template because it never changes:
- The base case: the simplest input, where the answer is known directly, no more recursion. - The recursive case: reduce the problem toward the base case, and call yourself on the smaller piece.
The classic first example is factorial (n! = n times (n-1) times ... times 1). Read the two comments and you will see the template staring right back at you:
js function factorial(n) { if (n u64 { let mut total = 0; for i in 1..=n { total += i; // a loop: constant stack usage, no overflow ever } total }
fn main() { println!("{}", sumto(1000000)); // 500000500000 }
Go is the interesting outlier. Goroutines start with a tiny stack (a few kilobytes) that GROWS automatically as needed, up to a large limit (a gigabyte by default on 64-bit systems). So Go tolerates FAR deeper recursion than JavaScript or Python before it ever complains -- the stack quietly resizes under you:
go package main
import "fmt"
func sumTo(n int) int { if n == 0 { return 0 } return n + sumTo(n-1) // Go's growable stack handles very deep recursion }
func main() { fmt.Println(sumTo(1000000)) // works - the goroutine stack grew to fit }
So the same recursive function overflows quickly in JS and Python, less quickly (but still) in Rust, and survives enormous depths in Go. The takeaway is not "Go is best" -- it is that stack depth is an implementation reality of your runtime, not a property of recursion itself. As an aside for the curious: the JavaScript spec actually DID add proper tail calls in ES2015, which would let certain recursions run in constant stack space, but in practice only Safari's engine ever shipped it -- V8 (so Node and Chrome) never did. That is why, in the JavaScript you will actually run, deep linear recursion overflows and a loop is the pragmatic answer. Having said that, for nested and branching data, recursion remains the clear winner in every one of these languages. ;-)
Try it yourself
Three exercises, increasing in difficulty. Write the base case FIRST every time, then the recursive case -- and predict the output before you run it. Full solutions open the next episode.
1. Write a recursive power(base, exponent) that computes base to the exponent (assume a non-negative integer exponent). Identify your base case explicitly in a comment. Test power(2, 10). 2. Write a recursive function flatten(arr) that turns an arbitrarily nested array of numbers into a flat array, so flatten([1, [2, [3, 4]], 5]) returns [1, 2, 3, 4, 5]. (Hint: check Array.isArray and concatenate results.) 3. Deliberately write a recursion with no base case and describe (do not necessarily run it to a crash) what error you would get and why. Then add a base case that stops it, and explain in words how the call stack grows and then shrinks for a small input like 3.
So what did we actually cover?
- Recursion is a function calling itself; every recursive function needs a base case (where it stops) and a recursive case (reducing the problem toward the base). - The call stack tracks calls with frames; recursion stacks a new frame per self-call, growing deep on the way down and unwinding on the way back up. - Too much depth (or a missing base case) causes a stack overflow, since the stack is finite (10k-15k frames in typical JS engines). - Recursion shines on naturally nested and branching data -- trees, nested arrays, nested objects -- where it mirrors the structure clearly and does not care how deep it goes. - Choose a loop for simple linear repetition (safe, no overflow) and recursion for nested or divide-and-conquer problems; stack behaviour differs across JS, Python, Rust, and Go.
Next episode we look at IIFEs and the module pattern, the clever pre-2015 technique that used functions and closures to create privacy and organize code before JavaScript had real modules.
See you in the next episode, happy recursing.
@scipio
Tags: #stem#stemsocial#steemstem#javascript#programming