Unhandled RangeError: "String length exceeds limit" — whole-state serialize in writeStagedState is not guarded by try/catch (crashes app on Hermes/React Native)
Summary
When the persisted state grows large enough that JSON.stringify of the combined
state exceeds the JS engine's maximum string length, redux-persist throws an
unhandled RangeError: String length exceeds limit. On React Native (Hermes)
this is a fatal, app-crashing error rather than a recoverable write failure.
The throw originates from defaultSerialize being called on the entire staged
state inside writeStagedState, and — unlike the per-key serialization path —
this call is not protected by a try/catch, so it escapes to the top of the JS
stack.
Environment
redux-persist: 6.0.0- Platform: React Native (Hermes engine)
- React Native version where the crash was captured: 0.83.9 (Hermes 0.14.1), Android
- React Native version currently in use: 0.85.3
- Storage backend: async key/value storage (
setItem/getItem)
Stack trace (symbolicated)
RangeError: String length exceeds limit
at processNextKey (createPersistoid)
at writeStagedState (createPersistoid)
at defaultSerialize (createPersistoid)
at JSON.stringify (native)Root cause
In createPersistoid:
function processNextKey() {
...
if (endState !== undefined) {
try {
stagedState[key] = serialize(endState); // ← per-key serialize IS guarded
} catch (err) {
console.error('redux-persist/createPersistoid: error serializing state', err);
}
}
...
if (keysToProcess.length === 0) {
writeStagedState();
}
}
function writeStagedState() {
...
// ← whole-state serialize is NOT guarded; a synchronous throw here is unhandled.
// `.catch(onWriteFail)` only catches async rejections from storage.setItem,
// not the synchronous RangeError from evaluating serialize(stagedState).
writePromise = storage.setItem(storageKey, serialize(stagedState)).catch(onWriteFail);
}
function defaultSerialize(data) {
return JSON.stringify(data); // throws "String length exceeds limit" for very large state
}serialize(stagedState) is evaluated before storage.setItem is called, so
when JSON.stringify throws synchronously, the .catch(onWriteFail) attached to
the setItem promise never runs. The exception propagates out of
writeStagedState (invoked from the throttled processNextKey/flush) with no
handler, and on Hermes it terminates the app.
Note the inconsistency: per-key serialization in processNextKey is wrapped in
try/catch, but the whole-state serialization in writeStagedState is not.
Expected behavior
A serialization failure (including exceeding the engine's max string length)
should be treated as a recoverable write failure and routed through the existing
failure handling (onWriteFail / the configured writeFailHandler), not thrown
as an unhandled fatal error.
Actual behavior
writeStagedState throws synchronously and the RangeError is unhandled,
crashing the application.
Suggested fix
Wrap the whole-state serialization in writeStagedState in a try/catch that
routes to onWriteFail (mirroring the guard already present in processNextKey),
so an oversized/failed serialization becomes a handled write failure instead of a
crash. For example:
function writeStagedState() {
Object.keys(stagedState).forEach(function (key) {
if (lastState[key] === undefined) delete stagedState[key];
});
let serialized;
try {
serialized = serialize(stagedState);
} catch (err) {
onWriteFail(err); // hand off to writeFailHandler instead of throwing
return;
}
writePromise = storage.setItem(storageKey, serialized).catch(onWriteFail);
}This keeps the library from crashing the host app and gives consumers a chance to
react to the failure via writeFailHandler.
Related
Similar large-state/serialization reports (memory/quota flavored, but not this specific unguarded synchronous throw):
- rt2zz/redux-persist#1155
- rt2zz/redux-persist#1104
- rt2zz/redux-persist#489
- rt2zz/redux-persist#185
Source: rt2zz/redux-persist