Verified against @stacksjs/stx@0.2.119, dist/signals.js.
useLocalStorage and useSessionStorage each parse and write storage with no error handling, on three code paths apiece. Any one of them takes down the entire client script for the page.
1. Initialiser — throws on any non-JSON value already at the key
// signals.js:3878
function useLocalStorage(key, defaultValue) {
var stored = localStorage.getItem(key);
var initial = stored !== null ? JSON.parse(stored) : defaultValue; // :3880Same at :3901 for useSessionStorage.
A plain string written by an earlier version of an app — or by any non-stx code sharing the origin — is enough. JSON.parse('list') throws SyntaxError, the composable never returns, and every statement after it in the client block is dead. There is no console hint pointing at the key.
Hit this in a real app: a key had previously held the bare string list. Migrating that page to useLocalStorage silently killed the whole script. The workaround was to burn the key name and move to <key>.v2, which is not something a storage composable should ever require.
2. Cross-tab storage handler — same parse, worse blast radius
// signals.js:3888
var handler = function(e) {
if (e.key === key) s.set(e.newValue !== null ? JSON.parse(e.newValue) : defaultValue);
};Same at :3908.
This one throws inside a window event listener, at an arbitrary later time, triggered by a different tab. It cannot be reproduced by loading the page, and the stack trace points into the runtime rather than at the consumer. Guarding only the initialiser would leave this live.
3. Write path — no try/catch, unlike its sibling eleven lines above
// signals.js:3882
effect(function() {
localStorage.setItem(key, JSON.stringify(s()));
});Same at :3904.
Compare useColorMode.persist in the same file:
// signals.js:3785
function persist(pref) { try { localStorage.setItem(storageKey, pref); } catch (e) {} }So two composables in one file take opposite positions on identical risk. Safari private mode, QuotaExceededError, and storage-disabled embedding contexts kill every useLocalStorage consumer while every useColorMode consumer survives. The difference is undocumented.
Suggested fix
Fall back to defaultValue when a read fails, and make writes non-fatal — matching the discipline readPersisted/persist already use:
function safeParse(raw, fallback) {
if (raw === null) return fallback
try { return JSON.parse(raw) } catch { return fallback }
}Apply at all six sites (:3880, :3888, :3882 and :3901, :3908, :3904). A console.warn naming the key on parse failure would turn a silent script death into a one-line diagnosis.
Happy to open a PR if the shape above looks right.