📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is recursion? Explain base cases, the call stack and stack overflow in .NET, then write a recursive and an iterative version of a tree or folder walk in C#. When should you avoid recursion?

Recursion fundamentals: base and recursive cases, how the call stack works, StackOverflowException in .NET, and converting recursion to iteration with an explicit stack.

Junior Coding Beginner algorithms call-stack recursion stack-overflow trees

🎯 Key points

  • Every recursive function needs a reachable base case, and every call must make progress towards it
  • Each call pushes a stack frame (return address, arguments, locals); stack use grows with recursion depth, not with total calls
  • Default stack size depends on the platform: about 1 MB on Windows and about 1.5 MB for .NET-created threads on Linux
  • In .NET a StackOverflowException cannot be caught: the runtime terminates the process, so one bad request can crash a web server
  • Any recursion can become a loop with an explicit `Stack<T>` on the heap (or `Queue<T>` for breadth-first order)
  • C# does not guarantee tail-call elimination, so tail recursion still uses stack space
  • Guard input-controlled depth with depth caps, `RuntimeHelpers.EnsureSufficientExecutionStack()`, parser limits such as `JsonSerializerOptions.MaxDepth`, and a visited set for cycles
  • Overlapping subproblems (naive Fibonacci) need memoisation or dynamic programming, or the call count grows exponentially

⏱ 30-second answer

Recursion is a function solving a problem by calling itself on a smaller version of it. Every recursive function needs:

  1. A base case that stops.
  2. A recursive case that moves towards the base case.

Each call pushes a frame on the thread's call stack (about 1 MB by default on Windows). Too deep and you get a StackOverflowException, which cannot be caught and kills the process. Recursion fits naturally recursive data (trees, nested JSON, folders), but use a loop with an explicit Stack<T> or Queue<T> when depth can be large or is controlled by input.

🗣 Full answer (senior)

Anatomy

static long Factorial(int n)
{
    if (n <= 1) return 1;            // base case
    return n * Factorial(n - 1);     // recursive case: smaller problem
}

When Factorial(4) runs, frames stack up until the base case, then unwind:

Factorial(4) waits for Factorial(3)
  Factorial(3) waits for Factorial(2)
    Factorial(2) waits for Factorial(1)
      Factorial(1) returns 1
    returns 2 * 1 = 2
  returns 3 * 2 = 6
returns 4 * 6 = 24

Where recursion is natural

  • Trees: org charts, category hierarchies, expression trees, Roslyn syntax trees.
  • Nested data: JSON documents, menus, comment threads.
  • Divide and conquer: merge sort, quicksort, binary search.
  • Backtracking: permutations, sudoku, path finding.

The stack and its limit

Each frame holds the return address, arguments and locals. The default stack is about 1 MB for threads on Windows and typically 1.5 MB for secondary threads on Linux with .NET. Real frames are often 100+ bytes, so depth in the tens of thousands can overflow. In .NET a StackOverflowException cannot be caught: the runtime terminates the process, so an ASP.NET Core app crashes and restarts, dropping all in-flight requests.

When to avoid recursion

  • Depth depends on user input or data you do not control (a 100,000-level linked chain, a malicious deeply nested JSON).
  • Tail-recursive "loops": C# does not guarantee tail-call elimination, so they still consume stack.
  • Overlapping subproblems (naive Fibonacci is O(2ⁿ)): use memoisation or dynamic programming.
  • Hot paths where call overhead matters.

Production example

A category API built breadcrumbs by recursing up ParentId. A data-migration bug created a cycle (A → B → A), recursion never reached a base case, and the process died with a stack overflow. Every pod crashed in a loop because each one received the same request. The fix was an iterative walk with a HashSet of visited ids and a maximum depth, plus a database check constraint to prevent the cycle.

🔍 What the interviewer is checking

Checks understanding of recursion mechanics and its failure modes in a real runtime.

Green flags

  • States base case and progress rules.
  • Explains the call stack and that StackOverflowException cannot be caught in .NET.
  • Converts recursion to an explicit Stack<T> loop correctly.
  • Mentions cycles, depth limits and user-controlled input.

Red flags

  • Writes recursion without a base case or with one that is unreachable for some inputs.
  • Thinks a try/catch will handle stack overflow.
  • Believes C# guarantees tail-call optimisation.
  • Uses recursion for exponential problems without memoisation.

📚 Learn in depth

1. The two rules

  1. Base case exists and is reachable for every input.
  2. Progress: every recursive call moves strictly closer to the base case (smaller n, child node, shorter list).

Break rule 2 (cycles, n not decreasing, negative input to factorial) and you get infinite recursion.

2. What a stack frame contains

high addresses
+------------------------+
| Main frame             |
+------------------------+
| Walk(root) frame       |  return address, args, locals, saved registers
+------------------------+
| Walk(child) frame      |
+------------------------+
| Walk(grandchild) frame |  <- stack pointer moves down as calls nest
+------------------------+
|   ... guard page ...   |  touching this = stack overflow
low addresses

Async methods are different: await in a recursive async method stores state in heap-allocated state machines, but synchronous completion still runs nested on the stack, so very deep async recursion can still overflow.

3. Recursion to iteration: the general recipe

Any recursion can be turned into a loop with an explicit stack that lives on the heap:

recursive DFS:              iterative DFS:
Visit(node):                stack.Push(root)
  process(node)             while stack not empty:
  for child in children:      node = stack.Pop()
     Visit(child)             process(node)
                              push children (reverse order to keep same order)

Swap the Stack<T> for a Queue<T> and you get breadth-first order instead.

4. Tail recursion in C#

static long SumTo(long n, long acc = 0) => n == 0 ? acc : SumTo(n - 1, acc + n);

This is tail recursive (the call is the last action), and the x64 JIT sometimes turns such calls into jumps, but the C# language and runtime do not guarantee it (it differs by platform, debug versus release, and method shape). Never rely on it; write the loop.

5. Recursion complexity

Use a recurrence:

Code Recurrence Result
Factorial T(n) = T(n−1) + O(1) O(n) time, O(n) stack
Binary search T(n) = T(n/2) + O(1) O(log n)
Merge sort T(n) = 2T(n/2) + O(n) O(n log n)
Naive Fibonacci T(n) = T(n−1) + T(n−2) + O(1) O(φⁿ) ≈ O(1.618ⁿ)
Tree walk each node once O(nodes), stack O(height)

6. Defending against hostile depth

  • System.Text.Json has JsonSerializerOptions.MaxDepth (default 64) so a deeply nested payload cannot blow your stack.
  • RuntimeHelpers.EnsureSufficientExecutionStack() throws a catchable InsufficientExecutionStackException before you overflow; Roslyn and Json use this idea.
  • Track visited nodes when data can contain cycles.
  • Cap depth explicitly in business hierarchies (for example 50 levels).

7. Try it yourself

// 1) Watch the stack overflow (process will crash; run in a throwaway console app)
// static int Down(int n) => Down(n + 1);  Down(0);

// 2) Safe iterative folder size
long total = 0;
var pending = new Stack<string>();
pending.Push(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
while (pending.Count > 0)
{
    var dir = pending.Pop();
    try
    {
        foreach (var f in Directory.EnumerateFiles(dir)) total += new FileInfo(f).Length;
        foreach (var d in Directory.EnumerateDirectories(dir)) pending.Push(d);
    }
    catch (UnauthorizedAccessException) { /* skip */ }
}
Console.WriteLine($"{total / 1_048_576} MB");

Note that Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories) already does this, but writing it yourself shows the pattern.

💻 Code

public sealed record Category(int Id, string Name, List<Category> Children);

// Recursive: clear, depth = tree height
static void PrintRecursive(Category node, int depth = 0)
{
    Console.WriteLine($"{new string(' ', depth * 2)}{node.Name}");
    foreach (var child in node.Children)
        PrintRecursive(child, depth + 1);
}

// Iterative: same order, stack on the heap, cannot overflow the thread stack
static void PrintIterative(Category root)
{
    var stack = new Stack<(Category Node, int Depth)>();
    stack.Push((root, 0));
    while (stack.Count > 0)
    {
        var (node, depth) = stack.Pop();
        Console.WriteLine($"{new string(' ', depth * 2)}{node.Name}");
        for (int i = node.Children.Count - 1; i >= 0; i--)   // reverse to keep left-to-right order
            stack.Push((node.Children[i], depth + 1));
    }
}

// Walking up a parent chain safely: cycle detection and depth cap
static List<string> Breadcrumbs(int id, IReadOnlyDictionary<int, (string Name, int? ParentId)> all, int maxDepth = 50)
{
    var path = new List<string>();
    var visited = new HashSet<int>();
    int? current = id;
    while (current is int c)
    {
        if (!visited.Add(c)) throw new InvalidOperationException($"Cycle at category {c}");
        if (path.Count == maxDepth) throw new InvalidOperationException("Hierarchy too deep");
        var (name, parent) = all[c];
        path.Add(name);
        current = parent;
    }
    path.Reverse();
    return path;
}

⚠ Common mistakes

  • Mistake: Missing or unreachable base case. Why it is wrong: The function recurses until the stack overflows, which in .NET terminates the whole process. Instead: Write the base case first, and check that every recursive call shrinks the input (including edge inputs such as negative numbers).

  • Mistake: Wrapping recursion in catch (StackOverflowException). Why it is wrong: Since .NET 2.0 the runtime does not let user code handle a stack overflow; the catch block never runs and the process dies. Instead: Prevent it: cap depth, call RuntimeHelpers.EnsureSufficientExecutionStack() (it throws a catchable InsufficientExecutionStackException), or iterate with an explicit stack.

  • Mistake: Recursing over data that may contain cycles, such as parent links or graph edges. Why it is wrong: A cycle means the base case is never reached. Instead: Track visited ids in a HashSet<int> and fail clearly when a cycle is found; also prevent cycles in the database.

  • Mistake: Relying on tail-call optimisation. Why it is wrong: C# and the JIT do not guarantee it; behaviour differs between Debug and Release builds and between platforms. Instead: Turn tail recursion into a while loop.

  • Mistake: Recomputing overlapping subproblems, as in naive Fibonacci. Why it is wrong: The number of calls grows exponentially, so even small inputs become very slow. Instead: Memoise results or use bottom-up dynamic programming.

  • Mistake: Recursing to a depth that the caller controls, such as nested JSON, XML or expressions, without validation. Why it is wrong: An attacker can send one deeply nested payload and crash the service. Instead: Validate input and enforce a maximum depth (for example JsonSerializerOptions.MaxDepth, default 64), or use an iterative algorithm.

🏛 Senior insight: trade-offs

A senior engineer chooses recursion for clarity on bounded, naturally recursive data and switches to iteration when depth is unbounded or controlled by input. The deciding fact in .NET is that a stack overflow is uncatchable and kills the process. In a web service, one bad payload or one corrupted hierarchy can become a crash loop across every pod, because each pod receives the same retried request.

Situation Choice Reason
Balanced tree, depth in the tens Recursion Clear code, stack use O(height) is tiny
Depth from user input or external data Iteration with Stack<T> or Queue<T> Heap memory, depth limited only by RAM
Parent chains that might contain cycles Loop with visited set and depth cap Turns a crash into a clear error
Overlapping subproblems Memoisation or tabulation Exponential becomes polynomial
Parsers and deserialisers Built-in depth limits Defends against hostile nesting
Hot path with measured call overhead Iteration, confirmed with BenchmarkDotNet Avoid guessing; measure

Seniors also design the data to protect the code: database constraints or triggers that stop cycles in parent-child tables, domain rules for maximum depth, and parser limits. They recognise recursion's relatives too: overlapping subproblems lead to dynamic programming, a queue instead of a stack gives breadth-first search, and graph data needs a visited set.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions naive Fibonacci or overlapping calls Why: Checks they know memoisation and dynamic programming
    How to answer

    Dynamic programming (DP) solves a problem by combining answers to smaller subproblems, storing each answer so it is computed once. It applies when:

    1. Overlapping subproblems: the same subproblem appears many times (naive Fibonacci recomputes fib(3) repeatedly).
    2. Optimal substructure: the best answer is built from best answers to subproblems.

    Two styles: memoisation (recursion plus a cache, top-down) and tabulation (fill a table in order, bottom-up, no recursion). Minimum coins: dp[a] = 1 + min(dp[a - c]) over coins c, O(amount × coins) time.

  2. When: Candidate converts recursion to an explicit stack Why: Extends the pattern to graph DFS with cycles
    How to answer

    DFS goes as deep as possible along one branch before backtracking. It uses recursion or an explicit stack, plus a visited set. O(V + E) time, O(V) space (the longest path).

    Key uses:

    • Cycle detection in directed graphs with three states: unvisited, in progress (on the current path), done. Reaching an in-progress node means a back edge, so a cycle.
    • Topological sort: add each node to a list after all its dependencies finish (post-order); that list is a valid build order.
    • Connected components, path existence, backtracking (permutations, sudoku).
  3. When: Candidate mentions StackOverflowException Why: Probes how to diagnose a stack overflow crash in production
    How to answer

    Each thread has a fixed-size stack (about 1 MB on Windows), and every method call pushes a frame. Unbounded or very deep recursion exhausts it. Since .NET 2.0 the runtime terminates the process on a stack overflow instead of throwing a catchable exception, because with no stack left it can't safely run handlers or finally blocks. To diagnose it, I read the crash output (recent .NET versions print the repeating frames to standard error), check container logs or the Windows Event Log, and capture a crash dump with DOTNET_DbgEnableMiniDump=1 to inspect the stack. Typical causes are a missing base case, a property or ToString that calls itself, cycles in object graphs (serialisation, Equals), and deeply nested input. Fixes: correct the recursion, switch to iteration with an explicit stack, and add depth limits and tests.

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me