Course outline · 0% complete

0/28 lessons0%

Course overview →

Why programs crash

lesson 4-2 · ~11 min · 12/28

The three classic memory crashes

With the layout in hand, the famous crashes stop being mysterious:

  1. Stack overflow. The stack has a fixed and fairly small size, a few megabytes. If function calls keep nesting without returning, frames pile up until the stack runs out of room, and the usual cause is runaway recursion.
  2. Out of memory, often written OOM. The heap can grow, but not forever. If a process keeps allocating, the OS eventually refuses, or a system OOM-killer terminates the process outright.
  3. Segmentation fault, usually shortened to segfault. The process touches an address it does not own, such as by following a bad pointer in C. The hardware traps it and the OS kills the process, which is process isolation from lesson 3-1 doing its job.
CrashRegion that ran out or was violated
stack overflowthe stack
out of memorythe heap
segfaultan address outside the process

Python protects you from segfaults and converts stack overflow into a catchable RecursionError, but the machine underneath is the same, which is why the same three failures show up in every language.

stack, frames pile upfree space between themheap, objects allocatenot this processtouching itis a segfaultstack overflow fills the top, out of memory fills the bottom
Each crash names the region that ran out of room or was touched without permission.

Recursion that returns

Each call pushes a frame and each return pops one, so even 500 levels deep is fine.

def countdown(n):
    if n == 0:
        return "liftoff"
    return countdown(n - 1)

print(countdown(5))
print(countdown(500))

Output

liftoff
liftoff

countdown(500) briefly stacks 501 frames, then they all pop as the returns unwind. The peak depth is what costs memory, not the total number of calls.

The base case is doing all the safety work here. if n == 0 is the only branch that returns without calling again, and it is reachable because every recursive call moves n one step closer to it.

Recursion that never returns

forever has no base case, so frames pile up until Python's stack limit stops it. Catching RecursionError shows the crash without ending the program.

import sys
print(sys.getrecursionlimit() > 0)

def forever(n):
    return forever(n + 1)

try:
    forever(1)
except RecursionError:
    print("crashed: the call stack overflowed")

Output

True
crashed: the call stack overflowed

sys.getrecursionlimit() is usually 1000, and Python counts frames against that limit rather than waiting for the real stack to run out.

That limit is a courtesy. In C the same mistake is a genuine stack overflow that the hardware catches, and there is no exception to handle, only a dead process, which is why the guard exists in the first place.

The base case that stops the recursion

The same countdown with the terminating branch in place.

def countdown(n):
    if n == 0:
        return "liftoff"
    return countdown(n - 1)

print(countdown(3))

Output

liftoff

The base case is the condition where the function returns without calling itself again, and it has to be placed before the recursive call so it can win.

Two things must both hold for recursion to be safe. There must be a base case, and every recursive call must move toward it, which countdown(n - 1) does. Writing countdown(n + 1) with the same base case would be just as broken as having no base case at all.

Reading the crash like an engineer

These are the symptoms as they appear in the wild, with the first thing to check for each:

SymptomLikely causeFirst thing to check
RecursionError or stack overflowrecursion with no reachable base casewhether the recursive call always moves toward the base case
process killed, MemoryError, machine crawls then a process diesunbounded growth in a list or cachewhich collection grows on every request or loop iteration
Segmentation fault (core dumped)native code touched memory it does not ownwhich C extension or native library was involved

The timing is a strong clue on its own. A stack overflow dies in milliseconds, a segfault dies at the exact instant of the bad access, and an out-of-memory death arrives hours or days in.

The pattern to internalize is that crashes are the OS and the hardware enforcing the rules from unit 1 and lesson 3-1, not random bad luck. Each one names a specific rule that was broken.

Diagnosing a service that dies after days

A web service that slowly uses more RAM over days until the OS kills it, and that a restart fixes for a while, is a memory leak filling the heap until out-of-memory.

Slow growth over time plus death by the OS is the signature of a heap leak. Something keeps references to objects, often a growing cache or list, so they can never be freed.

FamilyTime to failure
stack overflowmilliseconds
segfaultinstant, at the bad access
heap leak into OOMhours or days

A restart appears to fix it because a fresh process starts with an empty heap, which is why scheduled restarts are a common stopgap and never a fix. The real fix is finding the collection that only ever grows and giving it a bound.

Naming the runaway-recursion crash

A function that calls itself with a base case that can never be reached piles up frames until the program dies. That is a stack overflow.

Every unfinished call keeps a frame on the stack, and the stack region has a fixed size, so runaway recursion overflows it. Python surfaces this as RecursionError before the real stack dies.

The name states which memory region ran out of space, which is why it is the useful name rather than something generic such as "recursion error".

A very popular programming question-and-answer site is named after this crash, which is a decent hint at how often it happens.