Sentinel & Special Values — Find the Bug¶
Category: Resource & Type-Safety Patterns — 12 buggy snippets where a sentinel lies, leaks, or is mis-compared.
Table of Contents¶
- Bug 1: Overloaded
0for "no reading" - Bug 2:
-1for a value that can be negative - Bug 3: Forgotten sentinel check → out of bounds
- Bug 4:
err == io.EOFafter wrapping - Bug 5:
map.get == nullcan't tell absent from null - Bug 6:
NaNcompared with== - Bug 7:
Noneas default hides a realNone - Bug 8: Returning
nullcollection - Bug 9: Go nil interface is not nil
- Bug 10: Sentinel node exposed to caller
- Bug 11: Validation logs instead of failing
- Bug 12:
NaNpoisons a sort
Bug 1: Overloaded 0 for "no reading"¶
def read_temp(sensor) -> float:
if not sensor.ready():
return 0.0 # BUG: 0.0 °C is a real temperature
return sensor.value()
avg = (read_temp(a) + read_temp(b)) / 2 # absent sensors drag the mean down
Find the bug
`0.0` is a legal reading, so "no data" and "freezing" collapse into one value and leak into the average.Fix¶
from typing import Optional
def read_temp(sensor) -> Optional[float]:
return sensor.value() if sensor.ready() else None
Bug 2: -1 for a value that can be negative¶
// returns the account's balance delta, or -1 if no transactions
static int balanceDelta(Account a) {
if (a.transactions().isEmpty()) return -1; // BUG: -1 is a valid delta
return a.netChange();
}
Find the bug
A delta can legitimately be `-1` (a one-unit decrease), so `-1` is *inside* the domain — it collides with real data.Fix¶
static OptionalInt balanceDelta(Account a) {
return a.transactions().isEmpty() ? OptionalInt.empty()
: OptionalInt.of(a.netChange());
}
-1 is only safe when it is out of the valid domain (array indices), not for general integers. Bug 3: Forgotten sentinel check → out of bounds¶
Find the bug
The `-1` sentinel was not checked before indexing, so the absent case indexes out of bounds.Fix¶
Lesson: Sentinels are forgettable by design — the missed check is the whole risk.Bug 4: err == io.EOF after wrapping¶
_, err := readConfig(r) // wraps: fmt.Errorf("read: %w", io.EOF)
if err == io.EOF { // BUG: wrapping changed the value
// never runs
}
Find the bug
`%w` wrapping produces a new error value, so `==` against the sentinel fails.Fix¶
Lesson: Compare sentinel errors witherrors.Is, which walks the wrap chain. Bug 5: map.get == null can't tell absent from null¶
Integer v = scores.get(user);
if (v == null) {
scores.put(user, 0); // BUG: also fires when user is mapped to null on purpose
}
Find the bug
`get` returns `null` for *both* "key absent" and "key present, value null"; the branch can't distinguish them.Fix¶
Lesson:null is an overloaded sentinel inside maps — use containsKey / (value, ok). Bug 6: NaN compared with ==¶
Find the bug
IEEE-754 makes `NaN` unequal to everything, including itself, so `r == NaN` is never true.Fix¶
Lesson: DetectNaN with isNaN (or r != r), never ==. Bug 7: None as default hides a real None¶
def update(profile, nickname=None):
if nickname is not None: # BUG: caller can't intentionally set None
profile.nickname = nickname
update(p, nickname=None) # meant "clear it" — silently ignored
Find the bug
`None` is overloaded as both "not passed" and "explicitly None", so a caller cannot clear the field.Fix¶
_MISSING = object()
def update(profile, nickname=_MISSING):
if nickname is not _MISSING:
profile.nickname = nickname # None now means "clear it"
None is a legal argument, use a unique sentinel object for "not passed". Bug 8: Returning null collection¶
List<Order> orders(String id) {
if (!exists(id)) return null; // BUG: forces null checks everywhere
return db.query(id);
}
for (Order o : orders(id)) { ... } // NPE when null
Find the bug
Returning `null` instead of an empty list makes every caller null-check; one miss is an NPE.Fix¶
Lesson: Return empty collections, notnull (Effective Java, Item 54). Bug 9: Go nil interface is not nil¶
type myErr struct{}
func (*myErr) Error() string { return "x" }
func op() error {
var e *myErr // nil pointer
if false { e = &myErr{} }
return e // BUG: interface (type=*myErr, value=nil) != nil
}
if op() == nil { /* never runs */ }
Find the bug
A typed nil pointer returned as an interface yields a non-nil interface value; `== nil` is false.Fix¶
Lesson: From interface-returning functions, return a barenil, never a nil typed pointer. Bug 10: Sentinel node exposed to caller¶
class LinkedList:
def __init__(self): self.head = Node(None) # sentinel
def items(self):
node = self.head # BUG: starts at the sentinel
while node:
yield node.val # yields the sentinel's None first
node = node.next
Find the bug
Iteration starts at the dummy sentinel node, leaking its placeholder value to the caller.Fix¶
def items(self):
node = self.head.next # skip the sentinel
while node:
yield node.val
node = node.next
Bug 11: Validation logs instead of failing¶
def parse_port(s: str) -> int:
try:
return int(s)
except ValueError:
print("bad port") # BUG: logs, then...
return -1 # ...returns a sentinel callers won't check
Find the bug
On bad input it logs and returns `-1`; callers treat `-1` as a port and bind to a bogus value.Fix¶
def parse_port(s: str) -> int:
try:
p = int(s)
except ValueError as e:
raise ValueError(f"invalid port {s!r}") from e
if not (0 < p < 65536):
raise ValueError(f"port out of range: {p}")
return p
Bug 12: NaN poisons a sort¶
double[] xs = { 3.0, Double.NaN, 1.0, 2.0 };
Arrays.sort(xs, ...); // with a naive (a,b) -> a < b ? -1 : 1 comparator → BUG
// NaN breaks the comparator's contract → undefined order / IllegalArgumentException
Find the bug
A comparator using `<`/`>` returns inconsistent results around `NaN` (all comparisons false), violating the total-order contract.Fix¶
Arrays.sort(xs); // primitive sort: defined NaN ordering (NaN last)
// or for boxed:
list.sort(Comparator.comparingDouble(x -> x)); // uses Double.compare, NaN-aware
NaN lacks a total order; use NaN-aware comparators (Double.compare), not raw </>. Practice Tips¶
- Run
go test -raceandgo vet—vetcatches some nil-interface and==-on-error issues. - Grep for bare
-1,0,"", andnullreturns and ask: could this collide with real data? - Search for
== NaN,!= nullchains, and== io.EOF— classic sentinel mis-comparisons. - Test the absent case explicitly, not just the happy path.
← Tasks · Resource & Type-Safety · Roadmap · Next: Optimize
In this topic