◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-04-30Intermediate

How to Track Down 'undefined is not an object' Errors in Rork — Fast

Read Hermes' 'undefined is not an object' error correctly in Rork — five typical causes with code, plus debugging steps when stack traces look unhelpful.

Rork585Hermes9Debugging11Error Handling8React Native239

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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-10-13
Before You Ship: Go Looking for Your Own Keys Inside the Exported Expo Bundle
A key can be missing from your source and still be sitting inside the finished app. I ran expo export on Expo SDK 57, searched the bundle with grep and a short Node script, and recorded which secrets show up and which don't.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.