Two doors into the same app, and only one of them remembered to turn the lights on
A page crashed reading state that should have existed. The bug: a Redux slice got registered in exactly one bootstrap path, and a second, equally valid path to the same page never went through it.
By Meet ModiA feature worked perfectly for months, then started returning undefined for state that had never once been undefined in testing. Same feature, same code, same backend response. The only variable was which page the user had been on right before.
It reproduced 100% of the time on lazily-loaded pages and 0% of the time everywhere else, which is a very specific ratio for something that looks random at first glance.
The bug: the feature's Redux slice was registered in exactly one place, its own module's entry file. That file only ran on one of the two paths that could land a user on a page using that state.
Two ways to reach the same screen
The host app loaded independently-built modules at runtime. Most of the time, a module's own entry file ran first, and that entry file registered its slice into the store before anything tried to read from it:
// feature-module/src/index.ts
import { store } from "@host/store";
import { featureSlice } from "./featureSlice";
store.registerReducer("feature", featureSlice.reducer);
mountFeature();That worked because bootstrapping a module the normal way always ran this file. But some pages were loaded lazily, on demand, through a separate wrapper component whose whole job was to set up shared state for any page reachable that way before rendering it:
// LazyPageWrapper.tsx
function LazyPageWrapper({ children }: { children: React.ReactNode }) {
useEffect(() => {
store.registerReducer("otherFeature", otherFeatureSlice.reducer);
store.registerReducer("navigation", navigationSlice.reducer);
// "feature" was never added here
}, []);
return <>{children}</>;
}The wrapper had its own list of slices to register, maintained by a different team, for a different set of features. It had no reason to know this feature's slice existed, because nothing forced the two lists to stay in sync. They were two independent bootstraps that happened to both lead to a connected component reading from store.getState().feature.
The failure sequence
User navigates normally: host app mounts the module, index.ts runs, registerReducer("feature", ...) executes, state exists, connected component reads it fine.
User instead opens a deep link straight into a lazily-loaded page: LazyPageWrapper mounts, runs its own registration list, which doesn't mention "feature" at all, renders the page, the connected component tries store.getState().feature.someValue, and feature is undefined because nothing ever called registerReducer for it on this path. Reading a property off undefined throws.
"It's registered somewhere in the codebase" isn't the same claim as "it's registered before this component renders." The first one was true the entire time. The second one depended entirely on which door the user walked in through.
The fix
Register the slice in both places explicitly, rather than relying on one bootstrap path to have run before the other:
function LazyPageWrapper({ children }: { children: React.ReactNode }) {
useEffect(() => {
store.registerReducer("otherFeature", otherFeatureSlice.reducer);
store.registerReducer("navigation", navigationSlice.reducer);
store.registerReducer("feature", featureSlice.reducer); // now explicit here too
}, []);
return <>{children}</>;
}registerReducer was already idempotent, calling it twice for the same key was a no-op past the first call, so duplicating the registration cost nothing and closed the gap regardless of which path ran first.
What I should have done first
I should have treated "where does this slice get registered" as a question with potentially more than one right answer the moment I learned there were two separate bootstrap paths into the app. I only checked the path I'd personally tested on, which was the normal one, because that's the one I used every day.
The actual fix is one line. Finding it took longer than it should have because I kept looking at the feature module's own code, which was correct, instead of the wrapper that skipped it, which belonged to a different team's part of the codebase and wasn't the first place I thought to look.
More Posts
The vector database was innocent
I built a RAG service over the OpenTelemetry docs, then pointed OpenTelemetry back at it to find out why answers took 16 seconds. It wasn't the LLM. It wasn't the vector search either.
A query that returned nothing nested inside it
The exact same logical query returned full nested data through the REST layer and empty shells through the service layer underneath it. Same fields requested. Same backend. One shorthand parameter that only one of the two actually understood.
One field, three different envelopes, and a fix that made it worse
The same subscription-status field came back nested two levels deep, one level deep, or bare at the top, depending on account state. My fix didn't catch that. It added math on top of a check that was already wrong.