🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
java.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)
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();
}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 emptyDesign 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;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());
}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
Identical since introduction: message-less by construction (no-arg constructor, null detail) with a fresh instance allocated per underflow.
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.