An unreadable session snapshot should lock the session, not leave it open. We learned that the expensive way, when a timestamp that parsed to NaN left a session open forever.
The mechanics are worth a look, because this bug stays invisible in tests that only feed it valid data.
Session locking usually compares elapsed time against thresholds. You read the stored last-activity timestamp, subtract, and lock if the gap is bigger than the background or idle window. The logic is clean, as long as the subtraction gives you a number.
It does not give you one when the stored value is garbage. new Date on a malformed string hands you NaN, and NaN loses every comparison it enters. Not "greater" and not "less", both false. So the background check said no, the idle check said no, and the session stayed open. Worse, the countdown we exposed to the UI reported NaN, so nothing ever triggered the lock from that side either. Two independent safety paths, both defeated by one unparsable field.
In sessionlock, our open-source project, we fixed it by treating a stored snapshot as untrusted input instead of a typed value. Types describe what the compiler saw at build time. Storage holds whatever a previous version, a partial write, or a hand-edited record left behind.
So restore now asks whether the snapshot is readable at all, before it asks how old it is. Three checks, in order: is the state one of the two values we recognise, is the lock reason either absent or one of the three we know about, and does the timestamp (if present) actually parse to a finite number. A missing timestamp is fine. A present but nonsensical one is not.
An unreadable snapshot takes the locking branch, sharing it with the cases we already locked for. The reason falls back to a known value when the stored one is unrecognisable. Only a readable snapshot that is not already locked, and that carries a parsable timestamp, ever reaches the age comparison.
The general rule we keep re-deriving on money and auth systems: at every trust boundary, decide which way corruption should fail. Deserialised state is a trust boundary even when it never left the device. The default failure direction of silent corruption is almost always "open", with permissions not applied, locks not taken, limits not enforced, and "open" is the direction you cannot afford.
The check costs three comparisons at restore time. The bug cost a session that never locked.
Your persisted state is failing open somewhere today, and you probably would not notice it, because it looks exactly like nothing happened.