If you have edited a Rork-generated app for more than a few minutes, you have probably seen the red screen of death light up with undefined is not an object (evaluating 'user.profile.name'). The first time it happened to me, I assumed an optional-chaining sprinkle would make it go away. It rarely does. Most of the time, the property you are reaching for is fine — it is the parent object that arrived in a different shape than you expected.
This message comes from Hermes, the JavaScript engine Rork uses by default. It is telling you that the parent of the property you tried to access was undefined at evaluation time. The cluster of root causes is surprisingly small, so before reflexively adding ?. everywhere, it pays to learn how to read the error and recognize the patterns that produce it.
Always Read What's Inside the Parentheses
The most common mistake is reacting to the words undefined is not an object and ignoring everything after them. The interesting part is the trailing (evaluating 'user.profile.name') — Hermes is telling you exactly which expression failed.
ERROR undefined is not an object (evaluating 'user.profile.name')
Hermes' rule is: "I tried to read name, but user.profile was undefined." So the culprit is profile, not user or name. Internalizing this saves you from searching in the wrong place. When the expression has more than one dot, the left-most undefined operand is always where the chain broke. If evaluating 'a.b.c.d' fails because c is undefined, the message will still say (evaluating 'a.b.c.d'), so you cannot tell from the message alone which dot caused it. That is why the next section matters.
Step One: Stop One Level Before the Crash and Inspect
Stack traces in Rork builds tend to point at minified bundle locations like index.bundle:135432, which are not very helpful on their own. The technique I rely on is to log each step of the access chain right before the failure point.
// app/screens/Profile.tsx
function ProfileScreen({ route }) {
const userId = route.params?.userId;
const { data: user } = useUser(userId);
console.log('[Profile] user:', user);
console.log('[Profile] user.profile:', user?.profile);
if (!user?.profile) {
return <Text>Loading profile…</Text>;
}
return <Text>{user.profile.name}</Text>;
}When the logs fire, you usually find the answer immediately: user looks fine but user.profile is undefined. Adding ?. makes the screen render, but it does not tell you why profile was missing in the first place. I always confirm the why before shipping a defensive fix — otherwise the same shape mismatch will surface again two screens away.
Five Patterns That Cover Most Cases
1. The API Returns a Different Shape Than You Expect
This is the single most common source. The backend renames profile to userProfile, the frontend keeps reading the old name, and Hermes greets you with a red screen.
// Wrong: the response shape changed and the client has not caught up
const { data } = await fetch('/api/me').then(r => r.json());
return data.profile.name; // → undefined is not an object
// Right: confirm the actual shape, then narrow it
const json = await fetch('/api/me').then(r => r.json());
console.log('me response:', json);
const profile = json.user?.profile ?? json.profile ?? null;
if (!profile) throw new Error('profile was missing from /api/me');
return profile.name;The defensive ?? chain is not just paranoia — it gives you a graceful fallback while the backend rolls out a change. I prefer throwing a descriptive error over silently returning null because the loud failure is much easier to diagnose later.
2. useState With No Initial Value, Read on First Render
// Wrong: useState() leaves the value undefined on the first paint
const [user, setUser] = useState();
useEffect(() => { fetchUser().then(setUser); }, []);
return <Text>{user.email}</Text>; // crashes immediately
// Right: give it a sentinel and branch on the loading state
const [user, setUser] = useState(null);
useEffect(() => { fetchUser().then(setUser); }, []);
if (!user) return <ActivityIndicator />;
return <Text>{user.email}</Text>;This one is so common that I have a lint rule for it now. useState() with no argument is almost always a bug waiting to happen — make the initial value explicit, even if that means writing useState(null as User | null).
3. Forgetting That Nested Destructuring Throws on Missing Keys
This bites you most often inside navigation. Hooks like useRoute and useLocalSearchParams return undefined for keys that were never passed.
// Wrong: throws when params is empty
const { params: { userId } } = useRoute();
// Right: walk down with optional chaining and handle the empty case
const route = useRoute();
const userId = route.params?.userId;
if (!userId) {
if (__DEV__) console.warn('No userId on route', route);
return null;
}The __DEV__ guard is worth highlighting. In development, you want loud signals when assumptions break. In production, you want the screen to recover gracefully. Wrapping console.warn in __DEV__ gives you both behaviors without a feature flag.
4. Native Modules That Are Imported but Not Linked
Anything that needs Expo prebuild — @react-native-async-storage/async-storage is the classic example — can quietly resolve to undefined on a real device even when it works in Rork's preview.
import AsyncStorage from '@react-native-async-storage/async-storage';
if (__DEV__ && !AsyncStorage) {
console.error('AsyncStorage is undefined. Re-run expo prebuild.');
}A quick scan of the EAS Build log for Could not find module warnings often confirms the diagnosis in seconds. If your team uses a custom development client, double-check that the version installed on your test device actually contains the module — a stale dev client is a frustrating root cause that I have wasted hours on.
5. Reading a Value Before setState Has a Chance to Run
// Wrong: items is undefined on the first render
useEffect(() => {
fetch('/api/items').then(r => r.json()).then(setItems);
}, []);
return items.map(i => <Item key={i.id} item={i} />);
// Right: initialize with the correct empty container
const [items, setItems] = useState([]); // empty array, not undefined
return items.map(i => <Item key={i.id} item={i} />);For lists, useState([]) separates "still loading" from "loaded but empty" cleanly. For single objects, I prefer useState(null) and an explicit branch so the intent is obvious in code review.
When You Still Cannot Find It
If none of the above pinpoints the issue, the next move is to make the stack trace itself more legible. Connecting Rork to a Companion build enables source maps so the file and line in the trace start to look like your actual codebase. From there, wrapping suspected branches in small ErrorBoundary components is the fastest way to localize which subtree is throwing.
function SafeBox({ name, children }) {
return (
<ErrorBoundary fallback={<Text>{name} crashed</Text>}>
{children}
</ErrorBoundary>
);
}
<SafeBox name="ProfileHeader">
<ProfileHeader user={user} />
</SafeBox>
<SafeBox name="PostsList">
<PostsList userId={user?.id} />
</SafeBox>Turning "the whole screen is broken" into "the PostsList is broken" lets you place the next round of console.log calls exactly where they will be useful. I have shipped a couple of apps with permanent SafeBox wrappers around top-level navigation routes — the production cost is tiny and the developer experience boost is real.
A Tiny Helper That Pays for Itself
Once I started running into this error a few times a week, I added a small safeAccess utility that traces the path it walked and surfaces the exact step that returned undefined.
// utils/safeAccess.ts
export function safeAccess(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current == null) {
console.warn(`[safeAccess] '${path}' broke at '${key}'`);
return undefined;
}
current = current[key];
}
return current;
}
// Usage
const name = safeAccess(user, 'profile.address.city');
// Logs: [safeAccess] 'profile.address.city' broke at 'address'I do not use this in production hot paths — ?. is faster — but during a debugging session it is invaluable because it tells you in one line where the chain broke without recompiling.
Habits That Reduce Recurrence
The single biggest preventative measure I have found is strict TypeScript types for API responses, paired with a runtime validator like zod. The combination forces undefined possibilities to be acknowledged at the type level, and it catches schema drift the moment it shows up. If you find yourself fighting type errors as part of this work, my notes on stubborn TypeScript errors in Rork cover the patterns I lean on. For broader runtime crashes, the React Native / Expo runtime error guide maps out the rest of the territory.
A second habit worth forming: never destructure an object you have not yet asserted to exist. The combination of optional chaining and an explicit null check is verbose, but it pushes the failure mode into a place you have already thought about, which is the whole game.
Wrapping Up
Next time undefined is not an object shows up, resist the temptation to plaster ?. over the call site. Read the expression in the parentheses, log the parent one level up, and confirm whether the data shape itself is what you think it is. Then write the defensive code with full understanding of why it is needed. As a quick win for today: grep your project for useState() without an initial value — it is one of the easiest sources of this error to remove proactively.