Swift Recursion Limit and Stack Overflow: The Interview Guide

June 19, 20269 min read
dsaalgorithmsinterview-prepswift
Swift Recursion Limit and Stack Overflow: The Interview Guide
TL;DR
  • Swift has no configurable recursion limit: stack overflow terminates the process via SIGSEGV, with no catchable exception and no useful error message.
  • LeetCode runs Swift on an 8 MB stack, giving roughly 125,000 minimal frames, but real recursive functions burn stack 10 to 20 times faster due to value-type copies.
  • inout parameters cut per-frame cost in backtracking from full array copies down to 8-byte pointers, meaningfully extending safe recursion depth.
  • Swift tail call optimization is not guaranteed at the language level, unlike Kotlin's tailrec or Scala's @tailrec. Assume every recursive call consumes a full frame.
  • Iterative DFS with an explicit stack is the safe fix for large inputs: same algorithm, heap-allocated stack, no 8 MB ceiling.
  • In the interview, one sentence covers everything: name the O(n) depth, the 8 MB limit, and the iterative fallback as your mitigation.

Swift doesn't give you a warning. The Swift recursion limit isn't a setting you can adjust, and there's no exception you can catch when you hit it. Your recursive DFS overflows the stack, the process catches a SIGSEGV, and LeetCode prints "Runtime Error." That's the entire feedback loop. No stack trace, no line number, no apology.

Swift Has No Configurable Recursion Limit (and Doesn't Care)

Python gives you sys.setrecursionlimit(). Java gives you a configurable JVM thread stack. Swift gives you a hard look and nothing else.

Python at least has the decency to raise a RecursionError at 1,000 frames, like a bouncer who tells you exactly why you can't get in. Swift just lets you walk up to the door until the entire building collapses. The process terminates with SIGSEGV, which is Unix for "we're done here."

There is no setrecursionlimit() equivalent in Swift. The process terminates.

This matters in an interview because the failure mode is invisible. You won't see a helpful error message. You'll see "Runtime Error" with no stack trace, no line number, and no hint that recursion depth was the problem. Debugging this in real time is a fun way to spend your remaining 30 minutes.

The Numbers Behind "How Deep Is Too Deep"

Stack sizes in Swift depend entirely on the thread you're running on, not the language itself.

PlatformContextDefault Stack Size
Linux (LeetCode)Main thread8 MB
macOSMain thread8 MB
macOSSecondary threads512 KB
iOSMain thread1 MB
iOSSecondary threads512 KB

LeetCode runs Swift on Linux with an 8 MB default. For most interview problems with n up to 10,000, recursive solutions are fine. The danger zone is n = 100,000 with a worst-case input that maximizes depth: a completely skewed binary tree, or a graph that degrades into a long chain.

8 MB sounds generous until you think about what's actually in each frame.

Your Frame Size Is Lying to You

A bare-bones frame holds a return address, a saved frame pointer, and a handful of local variables. On 64-bit systems that's around 32 to 64 bytes. At 64 bytes per frame on an 8 MB stack, you'd get roughly 125,000 frames before anything goes wrong.

The issue is that most recursive functions in interview problems aren't bare-bones.

// Minimal frame: two pointer-sized values per call func dfs(_ node: TreeNode?) -> Int { guard let node = node else { return 0 } return 1 + max(dfs(node.left), dfs(node.right)) }
// Expensive frame: array parameter triggers a copy func explore(_ path: [Int], _ nums: [Int]) -> [[Int]] { if path.count == nums.count { return [path] } // ... }

Swift arrays are value types with copy-on-write, but when you pass a mutable-context array into a recursive call, you often get a real copy. Each frame in explore above carries its own version of path and nums. If those arrays have 20 elements, you're burning stack space 10 to 20 times faster than the rough estimate. Your effective recursion limit might be closer to 5,000 to 10,000.

See Swift Coding Interview Gotchas for more on how value-type semantics surface at scale.

Your try/catch Block Is Adorable and Will Not Help You

This is the sharpest difference from Python's RecursionError or Java's StackOverflowError. Both are catchable exceptions. Swift's stack overflow is an OS-level signal. Your do/try/catch is a polite conversation that never gets started.

// This does nothing useful when the stack overflows do { try deepRecursion(n) } catch { print("caught!") // never executes }

On Linux, the process receives SIGSEGV. On macOS and iOS, it's EXC_BAD_ACCESS. Either way, the process terminates immediately. Swift's exception machinery never gets involved. The program is simply gone.

What Causes a Stack Overflow covers why this signal can't be recovered from at the hardware level.

Swift Has No tailrec. This Is a Choice Someone Made.

Kotlin ships with tailrec. Scala ships with @tailrec. Both will either optimize your tail-recursive function or tell you at compile time that it can't be done. Swift has neither the keyword nor the error. It just silently does whatever it feels like.

A tail call is a recursive call that is the very last operation before the function returns:

// Potentially TCO-eligible: the recursive call IS the return value func sum(_ n: Int, _ acc: Int = 0) -> Int { if n == 0 { return acc } return sum(n - 1, acc + n) } // NOT tail-recursive: must compute max() after the calls return func height(_ node: TreeNode?) -> Int { guard let node = node else { return 0 } return 1 + max(height(node.left), height(node.right)) }

Swift uses LLVM and may perform TCO in some builds, but there is no language-level guarantee and no annotation to enforce it. Assume Swift will not optimize your recursive interview solution, and treat every recursive call as consuming a full stack frame. LeetCode's build configuration isn't documented, and you can't verify it mid-interview.

See Tail-Call Optimization for when languages can actually pull this off.

inout Actually Saves You Here

When you write backtracking solutions, passing large arrays as inout instead of by value significantly reduces per-frame stack usage. It's one of the few places where a Swift-specific pattern actively helps you in an interview context.

// Without inout: current and result get copied into each frame func backtrack(_ nums: [Int], _ current: [Int], _ result: [[Int]]) -> [[Int]] { ... } // With inout: only pointers (8 bytes each) pass through func backtrack(_ nums: [Int], _ current: inout [Int], _ result: inout [[Int]]) { if current.count == nums.count { result.append(current) return } for num in nums where !current.contains(num) { current.append(num) backtrack(nums, &current, &result) current.removeLast() } }

With inout, the frame carries a pointer to the original array rather than a copy. For n = 10 in a permutation problem, that's the difference between 10 × 80 bytes per frame and 10 × 8 bytes per frame. For backtracking, where depth reaches n, it adds up.

The Actual Fix: Stop Using the Call Stack

When worst-case depth is large, convert to iterative. The logic is identical. You manage your own stack on the heap instead of the call stack, and heap memory doesn't have the 8 MB constraint. You're not changing the algorithm. You're just choosing where to store your bookkeeping.

Iterative tree DFS:

func maxDepth(_ root: TreeNode?) -> Int { guard let root = root else { return 0 } var stack: [(TreeNode, Int)] = [(root, 1)] var result = 0 while !stack.isEmpty { let (node, depth) = stack.removeLast() result = max(result, depth) if let right = node.right { stack.append((right, depth + 1)) } if let left = node.left { stack.append((left, depth + 1)) } } return result }

Iterative graph DFS:

func numIslands(_ grid: [[Character]]) -> Int { var grid = grid var count = 0 let rows = grid.count, cols = grid[0].count for r in 0..<rows { for c in 0..<cols { guard grid[r][c] == "1" else { continue } count += 1 var stack = [(r, c)] grid[r][c] = "0" while !stack.isEmpty { let (row, col) = stack.removeLast() for (dr, dc) in [(-1,0),(1,0),(0,-1),(0,1)] { let nr = row + dr, nc = col + dc guard nr >= 0, nr < rows, nc >= 0, nc < cols, grid[nr][nc] == "1" else { continue } grid[nr][nc] = "0" stack.append((nr, nc)) } } } } return count }

Note var grid = grid at the top. Swift needs a mutable local copy since the parameter is a constant. Consider it a small tax for immutability-by-default.

Converting Recursion to Iteration covers the conversion patterns in depth.

When to Switch and When to Stay Recursive

Input SizeTree/Graph ShapeRiskRecommendation
n <= 1,000AnyMinimalRecursive is fine
n <= 10,000BalancedLowRecursive is fine
n <= 10,000Worst-case skewedModerateConsider iterative
n = 100,000AnyHighIterative DFS
Backtracking, n <= 20N/ANoneRecursive with inout
Memoized DP, n = 10,000N/ALow-moderateEither; iterative is safer

The Line to Deliver When the Interviewer Asks

If you write a recursive solution and your interviewer asks about worst-case behavior:

"The recursion depth here is O(n) in the worst case, which for n = 100,000 could overflow Swift's default 8 MB stack. If that's a concern, I'd convert this to an iterative solution using an explicit stack as a heap-allocated array. The algorithm is identical; we'd just be managing the stack ourselves."

That one statement covers stack size awareness, failure mode, and the fix. That's the full signal.

If you want to practice that kind of follow-up in a realistic setting, SpaceComplexity runs voice-based mock interviews with feedback on your problem-solving communication, not just whether your code compiles.

Further Reading