Loading learning content...
Every time you call pop() or peek() on an empty stack, you trigger an error. For robust code, you need a way to ask: 'Is there anything on this stack?' before attempting to access its contents.
This is the role of isEmpty: a simple boolean check that returns true if the stack contains no elements, and false if there's at least one element. Despite its simplicity, isEmpty is one of the most frequently called stack operations in real code, serving as the guard condition that prevents stack underflow errors.
By the end of this page, you will understand isEmpty's semantics and contract, why it's essential for defensive programming, how it relates to size(), its trivial O(1) implementations, and the patterns where isEmpty serves as a loop or branch condition. You'll recognize when to check isEmpty versus when it's unnecessary.
The isEmpty operation answers a single question: 'Does this stack contain zero elements?'
truefalseThis is a pure observational operation—it doesn't modify the stack in any way. You can call isEmpty as many times as you like without affecting the stack's state.
The logical relationship:
isEmpty() === true ⟺ size() === 0 ⟺ no top element exists
isEmpty() === false ⟺ size() > 0 ⟺ peek() and pop() are safe
isEmpty and size() are logically equivalent checks. isEmpty is preferred for clarity when you only care about 'empty or not,' while size() is preferred when you need the actual count.
12345678910111213
Operation Stack State isEmpty() Notes─────────────────────────────────────────────────────────────────(initial) [] true New stack is emptypush(10) [10] false Not empty anymorepush(20) [10, 20] false Still not emptypop() [10] false One element remainspop() [] true Empty againpush(5) [5] false Not emptypeek() [5] false peek doesn't change emptinesspop() [] true Back to empty Key observation: isEmpty becomes true ONLY after the last element is popped. isEmpty becomes false IMMEDIATELY after the first push to an empty stack.Before every pop() or peek() in defensive code, there should be an isEmpty() check (explicit or implicit through the design). The pattern 'if not empty, then pop' is so fundamental that some languages build it into the operation (returning Optional types).
isEmpty has the simplest contract of all stack operations—it has no preconditions and causes no side effects. It's truly a pure query.
| Aspect | Description |
|---|---|
| Input | None |
| Output | Boolean: true if empty, false otherwise |
| Side Effect | None — purely observational |
| Time Guarantee | O(1) in all implementations |
| Space Guarantee | O(1) — no allocations |
| Failure Mode | None — always succeeds |
isEmpty is the only stack operation that can never fail. Push might overflow. Pop and peek might underflow. But isEmpty is always safe to call on any stack in any state. This makes it the ideal guard condition.
Since isEmpty() is logically equivalent to size() === 0, you might wonder why both exist. The answer involves semantic clarity, performance considerations, and interface design philosophy.
Performance consideration:
In most implementations, isEmpty() and size() are both O(1) because a count is maintained. However, some implementations (particularly lazy ones or certain linked structures) might compute size() by traversing the entire structure (O(n)), while isEmpty() only needs to check if head is null or topIndex is -1 (always O(1)).
Even when both are O(1), isEmpty() is semantically clearer. Compare:
// Less clear: reader wonders 'why check size?'
if (stack.size() === 0) { ... }
// Crystal clear intent
if (stack.isEmpty()) { ... }
Design principle: Use the operation that best expresses your intent. If you're asking 'is it empty?', use isEmpty(). If you're asking 'how many elements?', use size().
Java's Collection interface includes both isEmpty() and size() for this reason. The Effective Java guideline recommends isEmpty() over size() == 0 because it's clearer and, in some collections, potentially more efficient.
In an array-based stack, isEmpty checks whether topIndex indicates no elements. With the standard convention of topIndex = -1 for empty, isEmpty is a simple comparison.
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 } /** * isEmpty: Check if the stack contains no elements * * Time Complexity: O(1) * Space Complexity: O(1) * * @returns true if stack has no elements, false otherwise */ isEmpty(): boolean { return this.topIndex < 0; // Equivalently: return this.topIndex === -1; } /** * Alternative implementation using size * (if size is tracked separately) */ isEmptyAlt(): boolean { return this.size() === 0; } /** * Size of the stack (for completeness) */ size(): number { return this.topIndex + 1; } // Push updates topIndex (incrementing from -1 to 0 on first push) push(element: T): void { this.topIndex++; // -1 → 0, then 0 → 1, etc. this.items[this.topIndex] = element; } // Pop checks isEmpty before proceeding pop(): T { if (this.isEmpty()) { throw new Error("Stack underflow"); } return this.items[this.topIndex--]; }}Why topIndex < 0 instead of topIndex === -1?
Both work correctly since topIndex should only ever be -1 when empty (not -2 or other negative values). However, < 0 is slightly more defensive—if a bug somehow set topIndex to -2, both conditions would catch it as 'empty.' In practice, the difference is negligible.
Execution trace:
1234567891011121314
State: topIndex = -1 (empty stack)isEmpty() → topIndex < 0 → -1 < 0 → true ✓ After push(10): topIndex = 0isEmpty() → topIndex < 0 → 0 < 0 → false ✓ After push(20): topIndex = 1isEmpty() → topIndex < 0 → 1 < 0 → false ✓ After pop(): topIndex = 0isEmpty() → topIndex < 0 → 0 < 0 → false ✓ (still has element) After pop(): topIndex = -1isEmpty() → topIndex < 0 → -1 < 0 → true ✓ (empty again)The isEmpty method body is literally a single expression: return this.topIndex < 0. This simplicity is ideal—there's almost no room for bugs. When methods are this trivial, consider inlining them in performance-critical code (though modern compilers often do this automatically).
In a linked list-based stack, isEmpty checks whether the head pointer is null. When head is null, there are no nodes in the list, meaning the stack is empty.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
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; // Optional: tracking size constructor() { this.head = null; this.count = 0; } /** * isEmpty: Check if the stack contains no elements * * Time Complexity: O(1) * Space Complexity: O(1) * * @returns true if stack has no elements, false otherwise */ isEmpty(): boolean { return this.head === null; } /** * Alternative: using count (if maintained) */ isEmptyViaCount(): boolean { return this.count === 0; } /** * Size of the stack * * O(1) if count is maintained (recommended) * O(n) if we must traverse the list to count (not recommended) */ size(): number { // With count tracking (O(1)): return this.count; // Without count tracking (O(n) - avoid this): // let size = 0; // let current = this.head; // while (current !== null) { // size++; // current = current.next; // } // return size; }}Visualizing isEmpty states:
1234567891011121314151617181920212223
State 1: Empty Stack head → null isEmpty() → head === null → true ✓ State 2: After push(10) head → [10 | null] isEmpty() → head === null → false ✓ (head points to a node, not null) State 3: After push(20) head → [20 | next] → [10 | null] isEmpty() → head === null → false ✓ (head still points to a node) State 4: After pop() head → [10 | null] isEmpty() → head === null → false ✓ (still one node) State 5: After pop() head → null isEmpty() → head === null → true ✓ (back to empty)For linked list stacks, you can check either head === null or count === 0. Both work if count is properly maintained. The head-null check is slightly more fundamental (it works even if you forget to update count), while the count check is consistent with how size() works.
isEmpty is the most common stack method call in typical algorithms. It appears in loop conditions, guard clauses, and validation checks. Understanding these patterns helps you write correct stack-based code.
while (!stack.isEmpty()) { process(stack.pop()); } — Process all elements until empty.if (!stack.isEmpty()) { return stack.peek(); } — Safely access the top only if it exists.return stack.isEmpty(); — Check if all elements were matched/consumed (e.g., balanced brackets).if (stack.isEmpty()) { handleEmpty(); } else { handleNonEmpty(stack.peek()); } — Different behavior based on state.while (!stack.isEmpty() && condition(stack.peek())) { stack.pop(); } — Pop while non-empty AND condition met.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Pattern 1: Drain the stack (process all elements)function processAll<T>(stack: Stack<T>, process: (item: T) => void): void { while (!stack.isEmpty()) { const item = stack.pop(); process(item); }} // Pattern 2: Guard before accessfunction safePeek<T>(stack: Stack<T>): T | null { if (stack.isEmpty()) { return null; // Or throw, or return Optional } return stack.peek();} // Pattern 3: Validation — balanced parenthesesfunction isBalanced(s: string): boolean { const stack: string[] = []; for (const char of s) { if (char === '(') { stack.push(char); } else if (char === ')') { if (stack.length === 0) return false; // isEmpty check stack.pop(); } } // Final isEmpty check: all openers should be matched return stack.length === 0; // isEmpty() equivalent} // Pattern 4: Conditional pop with isEmpty + peek checkfunction nextGreaterElements(nums: number[]): number[] { const result = new Array(nums.length).fill(-1); const stack: number[] = []; // indices for (let i = 0; i < nums.length; i++) { // Continue while stack is non-empty AND current > top while (stack.length > 0 && nums[i] > nums[stack[stack.length - 1]]) { const idx = stack.pop()!; result[idx] = nums[i]; } stack.push(i); } return result;} // Pattern 5: Different behavior for empty vs non-emptyfunction getTopOrDefault<T>(stack: Stack<T>, defaultValue: T): T { if (stack.isEmpty()) { return defaultValue; } return stack.peek();}The most common pattern is if (!isEmpty()) { ... peek/pop ... }. This two-step guard-then-access pattern should become automatic. Even when you're confident the stack isn't empty, the explicit check documents your assumption and prevents subtle bugs.
While isEmpty itself is trivially simple, mistakes in its usage are common. These errors often manifest as stack underflow exceptions or incorrect algorithm behavior.
stack.pop(); if (stack.isEmpty()) ... — The isEmpty check comes too late; if the stack had one element, you've already consumed it and now checking emptiness of the wrong state.if (isEmpty()) vs if (!isEmpty()) — Double-check your logic. 'If empty, don't pop' vs 'If not empty, then pop.'1234567891011121314151617181920212223242526272829303132333435363738394041
// ❌ Mistake 1: Forgetting to checkfunction badProcess(stack: Stack<number>): number { return stack.pop(); // Crashes if stack is empty!} // ✅ Fix: Always guardfunction goodProcess(stack: Stack<number>): number | null { if (stack.isEmpty()) return null; return stack.pop();} // ❌ Mistake 2: Checking after consumingfunction badPeekAfterPop(stack: Stack<number>): void { const value = stack.pop(); // Might be the last element if (!stack.isEmpty()) { console.log("Next is:", stack.peek()); // Might miss the case where value WAS the last }} // ❌ Mistake 3: Race condition (pseudocode)// Thread A: if (!stack.isEmpty()) {// Thread B: stack.pop(); // Empties the stack// Thread A: stack.pop(); // CRASH! Stack is now empty // ✅ Fix: Synchronized accessfunction threadSafePop<T>(stack: Stack<T>, lock: Lock): T | null { lock.acquire(); try { if (stack.isEmpty()) return null; return stack.pop(); } finally { lock.release(); }} // ❌ Mistake 4: Logic inversionfunction wrongLogic(stack: Stack<number>): void { if (stack.isEmpty()) { stack.pop(); // BUG! Should be !isEmpty() }}Many bugs only appear when the stack is empty. Always test your stack algorithms with: (1) empty input, (2) single-element input, (3) input that causes the stack to become empty mid-algorithm. These edge cases catch most isEmpty-related bugs.
isEmpty is the simplest yet one of the most critical stack operations. It's the defensive barrier that prevents underflow errors and the logical foundation for stack-processing loops.
if (!isEmpty()) pattern is fundamental.while (!isEmpty())), validation checks (return isEmpty()), and branching decisions all rely on isEmpty.What's Next:
We've covered the boolean check isEmpty. The final operation in the stack interface is size(): returning the number of elements in the stack. While often implemented alongside isEmpty, size() serves different use cases and completes our understanding of stack state inspection.
You now fully understand isEmpty—its semantics, implementations, relationship with size(), and critical role in stack algorithms. Combined with push, pop, and peek, isEmpty gives you the defensive tools to write robust stack code. Next: size(), the quantitative view of stack state.