Stacks
Reading time: 25 minutes
A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. The last element you add is the first one you remove. Think of a stack of plates in a cafeteria: you add plates to the top and take plates from the top. You cannot grab a plate from the middle without first removing all the plates above it.
Stacks appear everywhere in computing. Your browser's back button, the undo feature in text editors, and the way your programming language tracks function calls all rely on stacks. This page teaches you how stacks work, how to implement them, and when to use them in your own code.
The LIFO principle
LIFO (Last In, First Out) defines the core behavior of a stack. When you add elements 1, 2, and 3 to a stack in that order, removing elements gives you 3, 2, and 1. The last element in is the first element out.
This ordering constraint is what makes a stack a stack. Unlike an array where you can access any element by index, a stack only lets you interact with the top element. You cannot peek at the middle or remove from the bottom without first removing everything above.
The LIFO constraint might seem limiting, but it is exactly what many problems need. When you undo an action in a text editor, you want to reverse the most recent action first. When a function calls another function, the inner function must finish before the outer function can continue. LIFO behavior matches these natural patterns.
Consider this sequence of operations:
Push
Aonto the stack. Stack is now:[A](A is on top)Push
Bonto the stack. Stack is now:[A, B](B is on top)Push
Conto the stack. Stack is now:[A, B, C](C is on top)Pop from the stack. Returns
C. Stack is now:[A, B]Pop from the stack. Returns
B. Stack is now:[A]Push
Donto the stack. Stack is now:[A, D]Pop from the stack. Returns
D. Stack is now:[A]
Notice how the element that comes out is always the most recently added one. This predictable ordering is the foundation for every stack application.
Stack operations
A stack supports a small set of well-defined operations. Understanding these operations and their costs helps you use stacks effectively.
Push
Push adds an element to the top of the stack. After a push, the new element becomes the top, and the previous top moves down one position.
Push should be a constant-time operation, O(1). You add to one end of the structure without touching other elements. Both array-based and linked-list-based implementations achieve this.
Pop
Pop removes and returns the element at the top of the stack. After a pop, the element below the removed one becomes the new top.
Pop should also be a constant-time operation, O(1). You remove from one end without touching other elements.
Popping from an empty stack is an error condition. Your implementation must decide how to handle this: return a special value like null, throw an exception, or leave behavior undefined. The examples in this page throw an error to make bugs obvious.
Peek (or Top)
Peek returns the top element without removing it. The stack remains unchanged after a peek.
Peek is useful when you need to examine the top element before deciding whether to pop it. Like push and pop, peek should be a constant-time operation, O(1).
Peeking an empty stack is also an error condition. Handle it the same way you handle popping an empty stack.
isEmpty
isEmpty returns true if the stack contains no elements, false otherwise.
This operation is essential for avoiding errors when popping or peeking. Always check isEmpty() before popping if you are not certain the stack has elements.
Size (optional)
Some stack implementations provide a size operation that returns the number of elements currently in the stack. This is convenient but not essential; you can always track size separately if your implementation does not provide it.
Array-based implementation
The most common stack implementation uses an array as the underlying storage. Elements live in a contiguous block of memory, and a variable tracks the position of the top.
Basic structure
Using JavaScript's built-in array methods
JavaScript arrays already have push() and pop() methods that operate on the end of the array. You can use these directly for a simpler implementation:
This version is cleaner and relies on JavaScript's optimized array implementation. The built-in push() and pop() methods handle resizing automatically.
How it works
The array stores elements from index 0 upward. The top variable (or items.length - 1 in the simplified version) tracks where the most recent element lives.
When you push:
Increment the top pointer (or let
push()do this automatically)Store the new element at that position
When you pop:
Read the element at the top position
Decrement the top pointer (or let
pop()handle this)Return the element
All operations access only one array position, so they run in constant time.
Resizing considerations
JavaScript arrays resize automatically, but understanding what happens under the hood is valuable.
When you push to a full array, the runtime must:
Allocate a new, larger array (typically 1.5x or 2x the current size)
Copy all existing elements to the new array
Add the new element
This resizing takes O(n) time, but it happens infrequently. If you double the size each time, you copy n elements only after n pushes since the last resize. The amortized cost of push remains O(1): each element is copied at most O(log n) times total, and the average per-operation cost is constant.
In performance-critical code, you can avoid resizing by pre-allocating an array of known size. Most applications do not need this optimization.
Advantages of array-based stacks
Memory efficiency. Elements are stored contiguously with no pointer overhead.
Cache friendliness. Sequential memory access patterns are fast on modern CPUs.
Simple implementation. Arrays are familiar and well-supported in every language.
Random access if needed. Though you should not rely on this (it breaks the stack abstraction), you can access any element for debugging.
Disadvantages of array-based stacks
Wasted space. Dynamic arrays often over-allocate to avoid frequent resizing.
Resize cost. Occasional resizing takes O(n) time, which can cause latency spikes.
Fixed capacity option. If you use a fixed-size array, you must handle stack overflow.
Linked list implementation
An alternative implementation uses a linked list. Each element lives in a separate node that points to the node below it in the stack.
Basic structure
How it works
The topNode pointer always references the most recently added element. Each node's next pointer references the node that was on top before it was pushed.
When you push:
Create a new node with the element
Set the new node's
nextto the current topUpdate
topNodeto point to the new node
When you pop:
Save the value from the top node
Update
topNodeto point totopNode.nextReturn the saved value (the old top node becomes garbage)
All operations update only a few pointers, so they run in constant time. Unlike array-based stacks, there is no resizing—each push allocates exactly one node.
Advantages of linked list stacks
No resizing. Each push allocates exactly the memory needed, no more.
Consistent performance. Every operation takes the same time; no occasional slowdowns from resizing.
Unbounded size. The stack grows until you run out of memory.
Disadvantages of linked list stacks
Memory overhead. Each element requires a separate node with a pointer, roughly doubling memory usage for small values.
Poor cache locality. Nodes may be scattered in memory, causing cache misses.
Allocation cost. Each push allocates memory; each pop frees memory. This can add up in high-frequency scenarios.
Array vs linked list: when to use which
For most applications, an array-based stack is the better choice. It uses less memory, has better cache performance, and is simpler to implement. JavaScript's built-in arrays handle resizing efficiently, so you rarely need to worry about it.
Choose a linked list stack when:
You need guaranteed constant-time operations with no occasional O(n) spikes
Memory fragmentation is acceptable and cache performance is not critical
You are implementing a stack in a language without dynamic arrays
In practice, most production code uses array-based stacks. The theoretical disadvantages of occasional resizing rarely matter in real applications.
Time complexity summary
Operation | Array-based | Linked list |
|---|---|---|
push | O(1) amortized | O(1) |
pop | O(1) | O(1) |
peek | O(1) | O(1) |
isEmpty | O(1) | O(1) |
size | O(1) | O(1) |
Both implementations achieve constant-time operations. The array-based version has amortized O(1) push due to occasional resizing, but this rarely matters in practice.
The call stack
One of the most important uses of stacks in computing is the call stack, which your programming language uses to manage function calls.
How the call stack works
When a function is called, the runtime pushes a stack frame onto the call stack. This frame contains:
The return address (where to continue after the function finishes)
Local variables for the function
Parameters passed to the function
Any saved registers or state
When the function returns, its frame is popped from the stack, and execution continues at the return address.
The call stack during execution:
greet("Alice")is called. Push greet's frame. Call stack:[greet]greetcallscreateGreeting("Alice"). Push createGreeting's frame. Call stack:[greet, createGreeting]createGreetingreturns. Pop its frame. Call stack:[greet]greetcallsconsole.log(). Push console.log's frame. Call stack:[greet, console.log]console.logreturns. Pop its frame. Call stack:[greet]greetreturns. Pop its frame. Call stack:[]
Stack overflow
The call stack has a limited size. If you call functions too deeply, you exhaust this space and get a stack overflow error.
Each recursive call adds a frame to the stack. Without a base case to stop recursion, the stack grows until it overflows.
Why LIFO works for function calls
LIFO order is exactly what function calls need. When function A calls function B, B must complete before A can continue. When B calls C, C must complete before B can continue. The innermost function always finishes first—last in, first out.
This is why recursion works. Each recursive call waits for its nested calls to complete, and the stack naturally tracks this nesting.
Tail call optimization
Some languages optimize tail calls, where a function's last action is calling another function. Instead of pushing a new frame, the runtime reuses the current frame. This prevents stack overflow for tail-recursive functions.
JavaScript engines may or may not implement tail call optimization. Node.js does not reliably optimize tail calls, so you should not depend on this behavior. When stack depth is a concern, convert recursion to iteration.
Common stack applications
Stacks solve many problems naturally. When a problem involves reversing, nesting, or tracking history, a stack is often the right tool.
Undo and redo
Text editors, graphics programs, and many other applications use stacks to implement undo and redo.
The undo stack stores previous states. Undo pops the most recent state and pushes the current state onto the redo stack. Redo reverses this. When a new action occurs, the redo stack clears because you can no longer redo actions from a different timeline.
Browser history
The browser's back and forward buttons work similarly. The back button uses a stack of previously visited pages. Going forward uses a separate stack of pages you navigated back from.
Parentheses matching
Stacks excel at validating nested structures. The classic example is checking whether parentheses, brackets, and braces are balanced.
When you see an opening bracket, push it. When you see a closing bracket, pop and check that it matches. If the stack is empty when you try to pop, or if the popped character does not match, the string is unbalanced. At the end, the stack should be empty; leftover openers mean unclosed brackets.
This algorithm works because matching brackets have a LIFO structure: the most recently opened bracket must close first.
Expression evaluation
Stacks are fundamental to evaluating mathematical expressions. Compilers and calculators use stacks to handle operator precedence and parentheses.
Evaluating postfix (Reverse Polish Notation)
In postfix notation (also called Reverse Polish Notation or RPN), operators come after their operands. The expression 3 + 4 becomes 3 4 +. This notation eliminates the need for parentheses and makes evaluation straightforward with a stack.
The algorithm processes each token:
If it is a number, push it onto the stack
If it is an operator, pop two operands, apply the operator, and push the result
At the end, the stack contains exactly one value: the result.
Converting infix to postfix (Shunting Yard algorithm)
The Shunting Yard algorithm converts standard infix notation (like 3 + 4 * 2) to postfix notation. It uses two stacks: one for output and one for operators.
This algorithm respects operator precedence and associativity. Higher-precedence operators bind tighter, and right-associative operators (like exponentiation) group from right to left.
Depth-first search (DFS)
Depth-first search explores as far as possible along each branch before backtracking. While DFS is often written recursively, an explicit stack shows the underlying mechanism clearly.
The stack stores nodes to visit. Popping a node processes it; pushing neighbors schedules them for future processing. Because stacks are LIFO, the algorithm goes deep before going wide.
Compare this to breadth-first search, which uses a queue (FIFO) and explores level by level.
Backtracking problems
Many problems involve exploring possibilities and undoing choices when they lead to dead ends. A stack naturally tracks these choices.
When the algorithm reaches a dead end, it backtracks by popping from the stack. The previous state becomes the current state, and exploration continues from there.
String reversal
A stack naturally reverses the order of elements. Push all elements, then pop them all.
While JavaScript has simpler ways to reverse strings (str.split('').reverse().join('')), this demonstrates the reversal property of stacks.
Min stack
A min stack supports finding the minimum element in O(1) time. It uses an auxiliary stack to track minimums.
The min stack tracks the minimum at each level of the main stack. When you pop a value equal to the current minimum, you also pop from the min stack, revealing the previous minimum.
Edge cases and error handling
Robust stack implementations must handle edge cases correctly.
Empty stack operations
Calling pop() or peek() on an empty stack is an error. Your implementation should handle this explicitly:
Throwing an error is usually best because it makes bugs obvious. Silent failure can hide problems until they cause confusing behavior elsewhere.
Stack overflow (fixed-size stacks)
If you implement a fixed-capacity stack, you must handle the case where a push exceeds capacity:
Handling null and undefined values
Your stack should correctly handle null and undefined as valid values:
Be careful when using sentinel values. If you return undefined to indicate an empty stack, you cannot distinguish between an empty stack and a stack containing undefined.
Type safety
In JavaScript, stacks hold values of any type. If you need type safety, document the expected type and consider runtime checks:
Performance tips
Prefer array-based implementations
For most use cases, arrays outperform linked lists due to cache locality. JavaScript's built-in array methods are highly optimized.
Avoid unnecessary operations
Do not call isEmpty() just to throw an error if you will access the stack anyway:
However, for clarity, if (!stack.isEmpty()) is often preferred unless performance is critical.
Pre-allocate when size is known
If you know the maximum stack size, pre-allocating can avoid resize costs:
Use typed arrays for numeric data
For large stacks of numbers, typed arrays offer better performance:
Typed arrays have fixed size and contiguous memory, making them faster for numeric operations.
Common mistakes
Using shift/unshift for stack operations
JavaScript's shift() and unshift() operate on the beginning of an array. They require shifting all elements and run in O(n) time. Always use push() and pop() which operate on the end:
Forgetting to check for empty stack
Always check before popping when the stack might be empty:
Mutating returned values
If your stack stores objects, popping returns a reference. Mutating the returned object changes the object itself:
If you need isolation, clone objects when pushing or popping.
Modifying stack during iteration
If you iterate over a stack while modifying it, you can get unexpected results:
Collect changes and apply them after iteration, or be very careful about the termination condition.
Summary
A stack is a fundamental data structure that follows the Last In, First Out (LIFO) principle. The last element you add is the first element you remove.
Key takeaways:
Core operations are push (add to top), pop (remove from top), peek (view top without removing), and isEmpty. All run in O(1) time.
Array-based implementations are usually preferred for their simplicity, memory efficiency, and cache friendliness. JavaScript's built-in
push()andpop()methods make this easy.Linked list implementations offer consistent O(1) operations without resize spikes but use more memory and have worse cache performance.
The call stack is a critical application of stacks that manages function calls and returns in your programs. Understanding it helps you debug recursion and avoid stack overflow.
Common applications include undo/redo functionality, browser history, parentheses matching, expression evaluation, depth-first search, and backtracking algorithms.
Edge cases to handle include empty stack operations, stack overflow (for fixed-size stacks), and null/undefined values.
Stacks appear deceptively simple, but they unlock solutions to many problems. When you recognize the LIFO pattern in a problem—reversing order, tracking nested state, or managing history—reach for a stack.