Loading content...
Sometimes, you need to know what's at the top of the stack before deciding what to do. Should you pop it? Should you push something else first? Should you take a completely different action based on its value?
This is where peek (also called top in some implementations) becomes essential. Peek returns the top element without removing it—a read-only, non-destructive operation that leaves the stack completely unchanged. It's the observer pattern applied to stack access: look, but don't modify.
By the end of this page, you will understand peek's semantics and contract, why it's distinct from pop, how it's implemented, its complexity characteristics, edge case handling, and—most importantly—the algorithmic patterns where peek is essential. You'll recognize when to peek versus when to pop.
The peek operation returns the element at the top of the stack without modifying the stack in any way. After a peek:
Peek is idempotent—calling it once, twice, or a thousand times in a row has the same effect (none) on the stack. The only 'side effect' is that you now know what's at the top.
123456789101112131415161718192021
Initial state: [A, B, C] ← C is on top ↑ top peek(): Returns C [A, B, C] ← Stack unchanged! C still on top ↑ top peek(): Returns C ← Same result again [A, B, C] ← Still unchanged ↑ top pop(): Returns C [A, B] ← C is gone! B is now top ↑ top peek(): Returns B ← Different element now [A, B] ← Stack still unchanged after peek ↑ top Key insight: Multiple peeks are harmless. A single pop changes everything.Think of peek as asking 'what would pop return?' without actually popping. You get the information you need to make a decision, without committing to that decision. This is invaluable when your next action depends on the top element's value.
Peek has the simplest contract of the main stack operations—it's essentially a pure function (no side effects) with one precondition.
| Aspect | Description |
|---|---|
| Input | None (peek takes no parameters) |
| Output | The element at the top of the stack (without removing it) |
| Side Effect | None — the stack is not modified |
| Time Guarantee | O(1) in all implementations |
| Space Guarantee | O(1) — no allocations needed |
| Failure Mode | Error if called on empty stack |
The mathematical view:
In formal terms, for any non-empty stack S and any value x:
peek(S) = top element of S
S after peek(S) = S (unchanged)
push(S, x); peek(S) = x (most recently pushed)
push(S, x); pop(S); peek(S) = peek(S before push) (unchanged after matched push-pop)
These equalities define peek's behavior precisely and can be used to verify implementations.
Peek is essentially a pure function: given the same stack state, it always returns the same value, and it never changes anything. This makes peek safe to call anywhere, anytime (as long as the stack isn't empty), without worrying about side effects. Use it freely for inspection and decision-making.
In an array-based stack, peek simply returns the element at the current top index. There's no index manipulation, no element movement—just a direct array access. This makes peek trivially simple to implement.
The key insight: Peek reads items[topIndex] and returns it. That's the entire operation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
class ArrayStack<T> { private items: T[]; private topIndex: number; constructor(capacity: number = 1000) { this.items = new Array<T>(capacity); this.topIndex = -1; // -1 indicates empty stack } /** * Peek: View the top element without removing it * * Time Complexity: O(1) * Space Complexity: O(1) * * @throws Error if stack is empty * @returns The element at the top of the stack */ peek(): T { // Precondition check: stack must not be empty if (this.topIndex < 0) { throw new Error("Cannot peek: stack is empty"); } // Simply return the element at topIndex // No modification to topIndex or the array return this.items[this.topIndex]; } /** * Alternative: peek with null safety * Returns undefined if empty (but be careful with this pattern!) */ peekOrUndefined(): T | undefined { if (this.topIndex < 0) { return undefined; } return this.items[this.topIndex]; } /** * Alternative: peek with default value * Returns provided default if empty */ peekOr(defaultValue: T): T { if (this.topIndex < 0) { return defaultValue; } return this.items[this.topIndex]; }}Execution trace of peek():
123456789101112131415161718
Stack state: items: [10, 20, 30, _, _, ...] (capacity 1000) topIndex: 2 ← 30 is the top element Calling peek(): Step 1: Check precondition topIndex (2) >= 0? Yes, stack is not empty. Step 2: Return items[topIndex] return items[2] = 30 After peek(): items: [10, 20, 30, _, _, ...] ← Unchanged! topIndex: 2 ← Unchanged! The stack is in exactly the same state as before the peek.Calling peek() again would return 30 again.The peek method body (excluding error handling) is typically a single line: return this.items[this.topIndex]. This simplicity is a feature, not a limitation. Simple code is bug-free code.
In a linked list-based stack, peek returns the value stored in the head node. Since the head is the top of the stack, we simply access head.value without modifying any pointers.
The key insight: We return head.value without touching head.next or reassigning head. The list structure remains completely intact.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
class StackNode<T> { value: T; next: StackNode<T> | null; constructor(value: T) { this.value = value; this.next = null; }} class LinkedListStack<T> { private head: StackNode<T> | null; private count: number; constructor() { this.head = null; this.count = 0; } /** * Peek: View the top element without removing it * * Time Complexity: O(1) * Space Complexity: O(1) * * @throws Error if stack is empty * @returns The element at the top of the stack */ peek(): T { // Precondition check: stack must not be empty if (this.head === null) { throw new Error("Cannot peek: stack is empty"); } // Return the value from the head node // The head pointer itself is NOT modified return this.head.value; } /** * Alternative: safe peek that returns an optional type */ peekSafe(): { hasValue: true; value: T } | { hasValue: false } { if (this.head === null) { return { hasValue: false }; } return { hasValue: true, value: this.head.value }; }}Visualizing linked list peek:
1234567891011121314151617181920212223242526
Stack state (before and after peek): head → [30 | next] → [20 | next] → [10 | null] ↑ top Calling peek(): Step 1: Check precondition head !== null? Yes, head points to node with value 30. Step 2: Return head.value return 30 After peek(): head → [30 | next] → [20 | next] → [10 | null] ↑ top (same) Observation: - The head pointer still points to the same node - The node's next pointer is unchanged - The entire linked list structure is identical - The count is unchanged Contrast with pop(), which would move head to head.next: head ──────→ [20 | next] → [10 | null] ↑ new topIn both implementations, peek only reads data—it never writes. This makes peek inherently thread-safe for read operations (though you'd still need synchronization if other threads might push/pop concurrently).
At first glance, peek might seem redundant—'if you want the element, just pop it!' But this misunderstands the role of peek. There are many scenarios where you need to inspect the top element before deciding what to do with it.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Use Case 1: Conditional popping in monotonic stackfunction nextGreaterElement(nums: number[]): number[] { const result: number[] = new Array(nums.length).fill(-1); const stack: number[] = []; // stores indices for (let i = 0; i < nums.length; i++) { // PEEK to check condition before popping while (stack.length > 0 && nums[i] > nums[stack[stack.length - 1]]) { // Only now do we pop, because condition was satisfied const idx = stack.pop()!; result[idx] = nums[i]; } stack.push(i); } return result;} // Use Case 2: Bracket matching with peek-before-popfunction isValidBrackets(s: string): boolean { const stack: string[] = []; const matches: Record<string, string> = { ')': '(', ']': '[', '}': '{' }; for (const char of s) { if ('([{'.includes(char)) { stack.push(char); } else if (')]}';.includes(char)) { // PEEK first: is the top what we expect? if (stack.length === 0 || stack[stack.length - 1] !== matches[char]) { return false; // Mismatch detected via peek } stack.pop(); // Now safe to pop } } return stack.length === 0;} // Use Case 3: Undo preview (show what will be undone)class UndoManager<T> { private undoStack: T[] = []; previewUndo(): T | undefined { if (this.undoStack.length === 0) return undefined; // PEEK: show what would be undone, without actually undoing return this.undoStack[this.undoStack.length - 1]; } performUndo(): T | undefined { if (this.undoStack.length === 0) return undefined; // POP: actually undo (remove from stack) return this.undoStack.pop(); }}Without peek, you might pop an element, inspect it, then push it back if you don't want to consume it. This 'pop-inspect-push' pattern is wasteful (3 operations instead of 1) and error-prone (you might forget to push back). Peek eliminates this anti-pattern entirely.
Different languages and libraries use different names for this operation. Understanding these conventions helps you work fluently across ecosystems.
| Language/Library | Method Name | Returns | Notes |
|---|---|---|---|
| Java Stack | peek() | Element or throws EmptyStackException | Classic peek naming |
| C++ std::stack | top() | Reference to top element | Returns reference, not copy |
| Python (list as stack) | stack[-1] | Element or IndexError | No dedicated method; index access |
| JavaScript Array | arr[arr.length-1] | Element or undefined | No dedicated method |
| C# Stack<T> | Peek() | Element or throws | Also has TryPeek() |
| Go (slice) | stack[len(stack)-1] | Element or panic | No dedicated method |
| Rust Vec | .last() | Option<&T> | Returns Option for safety |
| Swift Array | .last | Optional element | Property, not method |
Key differences to be aware of:
Return semantics: C++ top() returns a reference to the element, allowing modification of the top element in-place. Java/C# peek() returns a copy (or the object reference for reference types).
Safety: Rust's last() returns Option<&T>, forcing you to handle the empty case. Java throws exceptions. Python crashes with IndexError.
Mutability: Some implementations let you modify the top element via peek/top (C++ reference). Others treat it as read-only inspection.
Best practice: When implementing your own stack, use peek() for clarity—it conveys 'looking without touching.' Reserve top() for contexts where C++ conventions dominate.
Stack operations seem universal but have subtle differences. Does peek() throw on empty, or return null? Does top() return a reference or a copy? Always verify with the specific library documentation when working in a new language or codebase.
Just like pop, calling peek on an empty stack is an error condition—there's no element to return. The strategies for handling this mirror those for pop, and the same trade-offs apply.
.hasValue or use .getOrElse().12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
class Stack<T> { private items: T[] = []; // Variant 1: Throwing (Java-style) peek(): T { if (this.items.length === 0) { throw new Error("EmptyStackException: Cannot peek empty stack"); } return this.items[this.items.length - 1]; } // Variant 2: Nullable (simple but risky) peekOrNull(): T | null { if (this.items.length === 0) return null; return this.items[this.items.length - 1]; } // Variant 3: Optional type (type-safe) peekOptional(): { present: true; value: T } | { present: false } { if (this.items.length === 0) { return { present: false }; } return { present: true, value: this.items[this.items.length - 1] }; } // Variant 4: Try pattern (C#-style) tryPeek(): { success: boolean; value?: T } { if (this.items.length === 0) { return { success: false }; } return { success: true, value: this.items[this.items.length - 1] }; } // Variant 5: Default value peekOr(defaultValue: T): T { if (this.items.length === 0) return defaultValue; return this.items[this.items.length - 1]; }} // Usage examples:const stack = new Stack<number>(); // Variant 1: Must handle exceptiontry { const top = stack.peek();} catch (e) { console.log("Stack was empty");} // Variant 3: Type-safe pattern matchingconst result = stack.peekOptional();if (result.present) { console.log(result.value); // TypeScript knows value exists} // Variant 5: Fallback valueconst topOrDefault = stack.peekOr(0); // Returns 0 if emptyIf your peek() throws on empty, your pop() should too. Inconsistent error handling (peek throws, pop returns null) confuses users and leads to bugs. Choose a strategy and apply it uniformly across all stack operations.
Peek completes the trio of core stack data operations (push, pop, peek). While simpler than push and pop, peek is essential for decision-making patterns that would otherwise require clumsy workarounds.
What's Next:
We've covered the three data operations: push (add), pop (remove), and peek (view). Next, we'll examine isEmpty: the essential guard that prevents stack underflow by letting you check whether any elements exist before attempting to pop or peek.
You now fully understand the peek operation—its semantics, implementations, use cases, naming conventions, and error handling. Combined with push and pop, peek gives you complete control over stack data access. Next: isEmpty, the defensive operation that prevents underflow errors.