🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGjava.util.EmptyStackException at java.base/java.util.Stack.peek(Stack.java:103) at java.base/java.util.Stack.pop(Stack.java:85) at com.devinhyderabad.editor.UndoManager.undo(UndoManager.java:41)

⚡ Quick Fix Works 80% of the time

Check isEmpty() before pop()/peek().

if (!undo.isEmpty()) { undo.pop(); }

🧠 Why this Happens

Tap to expand the deep technical explanation

Stack extends Vector: pop() delegates to peek(), which reads size() and throws when it is zero. Verified against the JDK sources (Temurin 25, Stack.java:103): peek() executes throw new EmptyStackException(), allocating a FRESH instance on every underflow - there is no shared or preallocated singleton anywhere in the class. Vector.elementAt is never reached on the underflow path because peeks own size guard fires first. The exception never carries a message for a separate reason: its constructor accepts no detail parameter at all, so there is simply nothing to print.

The HITEC City Parking Spot Analogy:

Grabbing the top plate from a cupboard someone already emptied.

🔁 How to Reproduce Confirm this is your error

Stack<String> undo = new Stack<>(); undo.push("a"); undo.pop(); undo.pop(); // EmptyStackException

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Guard with isEmpty()

👉 Use this if remaining on java.util.Stack and emptiness is an expected state.

One boolean check converts the crash path into a normal branch; pair pop and peek reads with the same guard.

if (!exprStack.isEmpty()) { char open = exprStack.peek(); exprStack.pop(); }
Solution 2

Migrate to ArrayDeque with pollFirst()/peekFirst()

👉 Use this if you want the modern replacement recommended by the Stack Javadoc itself.

Deque is the sanctioned stack interface since Java 6 docs guidance; pollFirst()/peekFirst() return null on empty instead of throwing, giving crash-free draining loops.

Deque<Character> st = new ArrayDeque<>(); st.push(c); // push Character top = st.pollFirst(); // null when empty
Solution 3

Design algorithms that cannot underflow

👉 Use this if writing parsers such as balanced-bracket matching.

On a closing delimiter with an empty stack, report "unmatched )" and stop: the empty stack IS the diagnostic, not an accident to survive.

case ')': if (st.isEmpty()) return fail("unmatched )"); st.pop(); break;
Solution 4

Drain with while (!isEmpty()), never do/while

👉 Use this if emptying a stack in bulk.

do/while pops once more than elements exist when the caller miscounts; condition-first loops test before every pop.

while (!st.isEmpty()) { process(st.pop()); }
Solution 5

Wrap a domain facade returning Optional<T>

👉 Use this if stacks cross module boundaries and callers should never see the exception class.

A tiny SafeStack exposing Optional popSafe() centralizes the guard once and documents emptiness in the API shape.

class SafeStack<T> { private final Deque<T> d = new ArrayDeque<>(); Optional<T> popSafe() { return Optional.ofNullable(d.pollFirst()); } }

📋 Version Notes

Java 8

Identical since introduction: message-less by construction (no-arg constructor, null detail) with a fresh instance allocated per underflow.

Java 11+

Unchanged. The Stack Javadoc still recommends Deque/ArrayDeque instead; ArrayDeque.pollFirst()/peekFirst() return null rather than throw.

🛡️ How to Prevent This Next Time

Prefer Deque/ArrayDeque over Stack for all new code. Guard pops at the algorithm level where emptiness has meaning, not with scattered try/catch.