printUpgradeWarning crashes the app when serializing props that contain a context object with a throwing getter (e.g. React Navigation)
Credits to where credits is due: this bug was found by Claude.
Describe the bug
NativeWind's dev-only "upgrade warning" can crash the app instead of logging. printUpgradeWarning calls stringify(originalProps), which deep-walks the props graph with Object.entries(value). Since Object.entries invokes getters, if any object reachable in that graph has a getter that throws, the warning itself throws and brings down the render.
This is easy to hit with React Navigation: its default NavigationStateContext value defines getters (getKey, getState, …) that throw "Couldn't find a navigation context" by design. React elements reference context objects via their type, so the serializer walks into the context value and triggers the throw. The result is a red screen with a misleading navigation error that has nothing to do with the real cause.
The crash originates in stringify (runtime/native/render-component):
for (const entry of Object.entries(value)) { // invokes getters — can throw
newValue[entry[0]] = replace(entry[0], entry[1]);
}triggered whenever a component enters SHOULD_UPGRADE (an animation/transition, CSS variable, or container style applied after the initial render) while canUpgradeWarn is true (dev only).
Observed stack (abridged):
CssInterop.View → renderComponent → printUpgradeWarning → stringify → replace → … → get getKey Error: Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?
Reproduction
Scaffold with npx rn-new@latest --nativewind --expo-router. On a screen rendered inside the navigator, render a component whose className adds a transform (or CSS variable / container style) only after the first render — e.g. a label that gains -translate-y-1/2 when a floated state flips on focus, reusing the same element slot across the toggle. Trigger the state change (focus the field). Instead of the expected upgrade warning, the app red-screens with Couldn't find a navigation context.
Expected behavior
The upgrade warning should log (or be skipped) without crashing. A debug serializer must never throw when a prop in the walked graph exposes a throwing getter — it should degrade gracefully (e.g. emit [Unserializable]) and still print the warning text.
Suggested fix — wrap the getter-invoking enumeration:
function stringify(object) { const seen = new WeakSet(); return JSON.stringify(object, function replace(_, value) { if (!(value !== null && typeof value === "object")) return value; if (seen.has(value)) return "[Circular]"; seen.add(value); const newValue = Array.isArray(value) ? [] : {}; let entries; try { entries = Object.entries(value); // invokes getters — may throw } catch { seen.delete(value); return "[Unserializable]"; } for (const entry of entries) { newValue[entry[0]] = replace(entry[0], entry[1]); } seen.delete(value); return newValue; }, 2); }
Additional context
Production is unaffected (canUpgradeWarn is gated on process.env.NODE_ENV !== "production"), but the dev experience breaks and the misleading error costs significant debugging time pointed at the wrong subsystem (React Navigation rather than NativeWind). Confirmed the vulnerable stringify is unchanged in [email protected], so it's not fixed by a patch bump. Versions:
nativewind ^4.2.1 react-native-css-interop ^0.2.1 (also verified 0.2.5) react 19.2.0 react-native 0.83.2 expo 55.0.6 / expo-router 55.0.5 @react-navigation/native ^7.1.8 (@react-navigation/core 7.14.0) react-native-reanimated ~4.2.1 Hermes, React Compiler enabled
Could be related to #1711
Source: nativewind/nativewind