skip to content

What should pop on an empty stack do — signal an error, or return a sentinel value like -1?

level: seniorimportance: should knowfreq 45%

answer

  1. what values can a reading legally take
  2. can the caller tell empty from data
  3. a sentinel must be outside the element domain
  4. signal failure out of band, not in the value
  5. an unguarded peek reads a stale slot

basics

~20 s

Signal the failure out of band. A sentinel is safe only when it lies outside the element domain, and for signed sensor readings -1 is a legal reading, so the caller cannot tell emptiness from data and the corruption is silent.

solid answer

~50 s

Underflow is a contract decision, and the deciding question is whether the caller can distinguish "nothing was there" from "an element was there". Signalling an error, or returning a two-part result of a validity flag plus a value, always answers that question. A sentinel answers it only if the sentinel provably lies outside the element domain — and for a stack buffering signed integer readings from a device, the domain is the whole range of the type, so every candidate sentinel is also a legal reading. The failure that follows is the bad kind: no crash, no log line, just a fabricated reading flowing into downstream aggregates, discovered weeks later as a drifting average. The related bug is `peek` without a guard: since pop typically only moves the top marker and leaves the vacated slot untouched, an unchecked peek returns a stale element that looks entirely valid.

code

pseudocode · 13 lines
pseudocode
// invariant: slots 0..top hold live elements; top == -1 means empty
pop():
    if top == -1
        signal Underflow        // never a sentinel from the element domain
    x = a[top]
    a[top] = unset              // optional: stop the stale slot lingering
    top = top - 1
    return x

peek():
    if top == -1
        signal Underflow        // without this guard, a vacated slot reads as data
    return a[top]

go deeper

for a junior

Know that removing from an empty stack is an error case that must be handled, and that the emptiness check belongs on both removal and inspection. Do not invent a magic return value for it.

for a middle

Explain why a sentinel is only valid when it lies outside the element domain, and walk through the contrast: an error names the offending call, while a fabricated value travels into downstream computations unnoticed.

for a senior

Argue the contract choice for a concrete element domain, name the silent-corruption failure mode end to end, and raise the stale-slot behaviour of an unguarded inspection during review.

for a principal

Own consistency across the codebase: one underflow convention on shared structures, enforced in review, so callers never have to remember which one this particular container chose. Be able to defend the migration cost of changing an existing sentinel contract.

## Underflow is a contract question, not an implementation detail `pop` and `peek` are *partial* operations: they are defined only when the stack is non-empty. Every implementation has to decide what happens outside that domain, and the choice is part of the published contract, not a detail. There are three defensible answers and one popular indefensible one. **1. Signal a failure out of band.** Removing from an empty stack raises an error the caller cannot ignore by accident. The value channel stays pure — anything returned through it is a real element. This is the default worth arguing for: the failure is loud, immediate, and attributable to the line that caused it. **2. Return a two-part result.** Give back a validity flag plus a value, or an optional/maybe result. The caller must destructure it to reach the element, so the empty case is impossible to skip silently. This suits code paths where emptiness is expected and routine — draining a buffer — rather than exceptional. **3. Make it a precondition and enforce it.** Document that the caller must check emptiness first, and treat a violation as a defect: assert in development builds, and still guard in production rather than reading out of range. This is the choice performance-critical code sometimes makes, and it is only honest when the guard exists somewhere and the tests cover the violation. **4. Return a sentinel drawn from the element type.** This is the one to push back on, and the reason is precise. ## Why the sentinel silently corrupts A sentinel works only if the value can never be a legitimate element — that is, only if the element domain is strictly smaller than the return type's domain. Consider the concrete case: a device buffers unacknowledged sensor readings as fixed-width signed integers. Readings can be negative — a temperature below zero, a signed delta from a baseline, a calibration offset. So -1 is a legal reading. So is 0. So is the minimum of the type. There is no value left over to mean "nothing". When the domain is full, a sentinel does not report emptiness; it *fabricates data*. The caller adds it to a sum, feeds it to an average, writes it to a log of readings. Nothing fails. The stack behaves. The defect surfaces later as an aggregate that drifts, and by then the causal line is thousands of operations in the past. Compare that with the error signal: same bug in the caller, but the failure names the exact call. Mainstream ecosystems have never converged here, which is a useful thing to know: some standard libraries raise on removing from an empty container while others leave it undefined behaviour that reads whatever memory is at hand — so "what does the library do?" is not a substitute for deciding what *your* contract does. ## The stale-slot companion bug In a contiguous realization, `pop` normally does not clear the vacated slot — it reads the value and moves the top marker back. That is correct and fast, because nothing above the marker is live by the invariant. But it has two consequences worth naming in review. First, an unguarded `peek` reads memory that is not live. If the marker has moved back but the slot still holds the previous element, `peek` returns a value that is stale rather than absent, and stale values pass every plausibility check a caller might apply. Second, the vacated slot keeps whatever it held reachable, which delays reclamation of anything that element referenced — a small leak that grows with the high-water mark of the stack rather than its current depth. Clearing the slot on pop costs one write and removes both problems; whether that trade is worth it depends on the element type, but it should be a decision rather than an oversight. ## What to say when asked Lead with the distinguishability test: *can the caller tell "empty" from "an element"?* Any contract that answers yes is defensible; any contract that answers no is a silent-corruption generator, no matter how well documented, because documentation does not make the value distinguishable at the point of use. Then name the domain check explicitly — "a sentinel is safe only when the element domain excludes it, and for full-range numeric readings nothing is excluded". Finally, mention that the same guard belongs on `peek`, because the slot a pop left behind still looks like data.

  • What if the interface can only return one value and cannot signal a failure at all?
    Then return a compound value — a validity flag paired with the element — so reaching the element forces the caller through the flag. If even that is impossible, make emptiness a documented precondition, assert on it in development builds, and keep a production guard that fails safe rather than reading out of range. What you never do is overload a value from the element domain, because that hides the failure inside legitimate-looking data.
  • Is a caller-checks-first precondition ever the right contract?
    Yes, in hot paths where the caller genuinely knows the stack is non-empty — a drain loop already guarded by an emptiness test, for instance. It is honest only when the violation is still caught somewhere: an assertion in development builds, a fuzz or boundary test that attempts the empty pop, and a production guard that fails loudly rather than reading past the live region. A precondition nobody enforces is just an undocumented sentinel.
  • Should pop clear the slot it vacates?
    It is a real trade rather than an obvious yes. Clearing costs one extra write per pop and removes two hazards: an unguarded peek can no longer read a plausible stale value, and the vacated slot no longer keeps whatever the element referenced reachable, which otherwise leaks in proportion to the stack's high-water mark. For small numeric elements the leak argument disappears and the write may not be worth it; for elements holding references it usually is.

Returning -1 for an empty stack is like a scale that reads zero when it is switched off: the number looks like a measurement, so nobody notices that nothing was ever weighed.

saying these in an interview costs you the question

  • Returns -1 for empty when readings can legitimately be -1
  • Says a sentinel is fine because it is documented
  • Claims empty pop should just return silently
  • Forgets the same guard applies to peek
  • Assumes a vacated slot is cleared automatically
  • Treats underflow as an implementation detail, not a contract

context