Unhandled RangeError: "String length exceeds limit" — whole-state serialize in writeStagedState is not guarded by try/catch (crashes app on Hermes/React Native)

Author: AjayFrancisTechversantCreated Jul 7, 2026Updated Jul 22, 2026

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:

javascript
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:

javascript
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