Problem Solved

React · cannot read property

Home › Courses › React › Problems › Cannot read property
⚛️ Problem · 5 min read

"Cannot read property of undefined."
Three causes, three fixes.

The most common runtime crash in React. Somewhere, a value is undefined or null, and you're accessing a property on it. The stack trace points to the crash — but the bug is usually upstream. This page covers the three causes, the three fixes, and the pattern that prevents this class of bug entirely.

Short answer: Guard before access. Use optional chaining (obj?.prop) for one-off access, early returns (if (!obj) return ) for whole sections, and default values ({'{items = []}'}) in props. The three causes: (1) data isn't loaded yet, (2) parent didn't pass a required prop, (3) API returned the wrong shape. Fix each at its source — don't scatter ?. everywhere.

✅ Solution 1 — Data isn't loaded yet

The most common cause: initial state is null while fetching.

When you fetch data, the initial state is usually null or undefined. During the first render, the data doesn't exist — but the component tries to render it anyway. The crash happens before the fetch resolves. Fix: guard the render with optional chaining or a loading state.

data-not-loaded.tsx
import { useState, useEffect } from 'react';

interface User {
id: string;
name: string;
email: string;
address: { city: string; zip: string };
}

// ─── BAD: crash on first render ──────────────
function BadProfile({ userId }: { userId: string }) {
const [user, setUser] = useStatenull>(null);

useEffect(() => {
fetch(/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);

// ❌ Crashes on first render: user is null, user.address throws
return 

{user.name} — {user.address.city}

; } // ─── GOOD 1: optional chaining ─────────────── function GoodProfile1({ userId }: { userId: string }) { const [user, setUser] = useStatenull>(null); useEffect(() => { fetch(/api/users/${userId}`) .then(r => r.json()) .then(setUser); }, [userId]); // ✅ Short-circuit — renders "Loading..." until data arrives return (

{user?.name ?? 'Loading...'} — {user?.address?.city ?? '—'}

); } // ─── GOOD 2: early return (recommended) ────── function GoodProfile2({ userId }: { userId: string }) { const [user, setUser] = useStatenull>(null); useEffect(() => { fetch(/api/users/${userId}`) .then(r => r.json()) .then(setUser); }, [userId]); // ✅ Early return — no null checks in the rest of the component if (!user) { return

Loading...

; } // user is User here — full access without ? return (

{user.name}

{user.email}

{user.address.city}, {user.address.zip}

); } // ─── GOOD 3: three-state discriminated union ── type LoadState = | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function BestProfile({ userId }: { userId: string }) { const [state, setState] = useState>({ status: 'loading' }); useEffect(() => { setState({ status: 'loading' }); fetch(/api/users/${userId}`) .then(r => r.json()) .then(data => setState({ status: 'success', data })) .catch(error => setState({ status: 'error', error })); }, [userId]); if (state.status === 'loading') return

Loading...

; if (state.status === 'error') return

Error: {state.error.message}

; // state.data is User — fully typed and non-null return

{state.data.name} — {state.data.address.city}

; } console.log(BadProfile, GoodProfile1, GoodProfile2, BestProfile);
import { useState, useEffect } from 'react';

function BadProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
return React.createElement('p', null, user.name, ' — ', user.address.city);
}

function GoodProfile1({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
return React.createElement('p', null,
user?.name ?? 'Loading...', ' — ', user?.address?.city ?? '—'
);
}

function GoodProfile2({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
if (!user) {
return React.createElement('p', null, 'Loading...');
}
return React.createElement('div', null,
React.createElement('p', null, user.name),
React.createElement('p', null, user.email),
React.createElement('p', null, user.address.city, ', ', user.address.zip)
);
}

function BestProfile({ userId }) {
const [state, setState] = useState({ status: 'loading' });
useEffect(() => {
setState({ status: 'loading' });
fetch(/api/users/${userId}`)
.then(r => r.json())
.then(data => setState({ status: 'success', data }))
.catch(error => setState({ status: 'error', error }));
}, [userId]);

if (state.status === 'loading') return React.createElement('p', null, 'Loading...');
if (state.status === 'error') return React.createElement('p', null, 'Error: ', state.error.message);

return React.createElement('p', null, state.data.name, ' — ', state.data.address.city);
}

console.log(BadProfile, GoodProfile1, GoodProfile2, BestProfile);

Initial state is null

useState(null) — first render has null. Any access crashes until data arrives.

Optional chaining for one-off

user?.name — returns undefined instead of throwing. Combine with ?? for defaults: user?.name ?? '...'.

Early return for sections

if (!user) return — narrows user for the rest of the component. No ?. needed downstream.

Discriminated union

loading | success | error — three explicit states. The compiler forces you to handle each. Impossible to access data in the wrong state.

✅ Solution 2 — Missing prop

The parent didn't pass a value the child expects.

A parent renders a child without passing a prop, or passes undefined explicitly. The child assumes the prop exists and crashes on access. Fix: default the prop in destructuring, or require it with TypeScript / prop types. The compiler catches this at call sites if you type it correctly.

missing-prop.tsx
import { useState } from 'react';

interface User { id: string; name: string }

// ─── BAD: assumes prop exists ────────────────
function BadUserList({ users }: { users?: User[] }) {
// ❌ If parent doesn't pass users, this crashes:
// Cannot read property 'map' of undefined
return 
    {users.map(u =>
  • {u.name}
  • )}
; } // ─── GOOD 1: default the prop ──────────────── function GoodUserList({ users = [] }: { users?: User[] }) { // ✅ Default value — users is always an array return
    {users.map(u =>
  • {u.name}
  • )}
; } // ─── GOOD 2: require the prop ──────────────── interface RequiredProps { users: User[]; // not optional — parent must pass it } function RequiredUserList({ users }: RequiredProps) { // ✅ The compiler ensures parent passes users return
    {users.map(u =>
  • {u.name}
  • )}
; } // ─── GOOD 3: guard the render ──────────────── function GuardedUserList({ users }: { users?: User[] }) { if (!users) { return

No users

; } // users is User[] here return
    {users.map(u =>
  • {u.name}
  • )}
; } // ─── GOOD 4: default + destructure in map ──── interface CardProps { title: string; items?: { id: string; text: string }[]; } function Card({ title, items = [] }: CardProps) { return (

{title}

{items.length === 0 ? (

No items

) : (
    {items.map(item =>
  • {item.text}
  • )}
)}
); } // ─── Parent call sites ─────────────────────── function App() { const [users] = useState([ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' } ]); return (
{/* ❌ BadUserList: forgot users prop → crash */} {/* ✅ GoodUserList: default [] prevents crash */} {/* ✅ RequiredUserList: TS forces the prop /} {/ — compile error */} {/* ✅ GuardedUserList: shows "No users" */}
); } console.log(BadUserList, GoodUserList, RequiredUserList, GuardedUserList, Card, App);
import { useState } from 'react';

function BadUserList({ users }) {
return React.createElement('ul', null, users.map(u => React.createElement('li', { key: u.id }, u.name)));
}

function GoodUserList({ users = [] }) {
return React.createElement('ul', null, users.map(u => React.createElement('li', { key: u.id }, u.name)));
}

function RequiredUserList({ users }) {
return React.createElement('ul', null, users.map(u => React.createElement('li', { key: u.id }, u.name)));
}

function GuardedUserList({ users }) {
if (!users) {
return React.createElement('p', null, 'No users');
}
return React.createElement('ul', null, users.map(u => React.createElement('li', { key: u.id }, u.name)));
}

function Card({ title, items = [] }) {
return React.createElement('div', null,
React.createElement('h2', null, title),
items.length === 0
? React.createElement('p', null, 'No items')
: React.createElement('ul', null, items.map(item => React.createElement('li', { key: item.id }, item.text)))
);
}

function App() {
const [users] = useState([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' }
]);
return React.createElement('div', null,
React.createElement(BadUserList),
React.createElement(GoodUserList),
React.createElement(GoodUserList, { users }),
React.createElement(RequiredUserList, { users }),
React.createElement(GuardedUserList),
React.createElement(GuardedUserList, { users })
);
}

console.log(BadUserList, GoodUserList, RequiredUserList, GuardedUserList, Card, App);

Default in destructuring

{ users = [] } — if the parent doesn't pass users, it's an empty array. No crash, no null checks.

TypeScript enforces required

users: User[] — not optional. The compiler errors at call sites that omit it. Catches the bug before runtime.

Early return for optional

users?: User[] + if (!users) return ... — explicit "empty state" render. Better UX than a crash.

Never assume

If a prop is optional, handle the missing case. If required, make it required. Don't mix — every optional prop is a potential crash.

✅ Solution 3 — API returned wrong shape

Server sent an error object instead of an array, or null instead of a user.

The type says User, but the actual response is { error: 'Not found' }. TypeScript trusts you — you typed as User, but the runtime value doesn't match. The crash happens on first access. Fix: validate the API response with a type guard at the boundary, then trust the type.

wrong-shape.tsx
import { useEffect, useState } from 'react';

interface User {
id: string;
name: string;
email: string;
}

// ─── Type guard for the API boundary ─────────
function isUser(x: unknown): x is User {
return (
typeof x === 'object' &&
x !== null &&
'id' in x &&
'name' in x &&
'email' in x &&
typeof (x as User).id === 'string' &&
typeof (x as User).name === 'string' &&
typeof (x as User).email === 'string'
);
}

// ─── BAD: cast without validation ────────────
function BadProfile({ userId }: { userId: string }) {
const [user, setUser] = useStatenull>(null);

useEffect(() => {
fetch(/api/users/${userId}`)
.then(r => r.json())
.then((data) => setUser(data as User)); // ❌ trusts the shape
}, [userId]);

// If server returns { error: 'Not found' }:
// user is typed User but is actually { error: ... }
// user.name → undefined, user.email → undefined
// No crash here (undefined is safe), but downstream code that does
// user.address.city will crash.

return 

{user?.name ?? 'Loading...'}

; } // ─── GOOD: validate at the boundary ────────── function GoodProfile({ userId }: { userId: string }) { const [user, setUser] = useStatenull>(null); const [error, setError] = useState<string | null>(null); useEffect(() => { fetch(/api/users/${userId}`) .then(r => r.json()) .then((data: unknown) => { if (!isUser(data)) { setError('Invalid response'); return; } setUser(data); // ✅ User — validated }) .catch(e => setError(e.message)); }, [userId]); if (error) return

Error: {error}

; if (!user) return

Loading...

; return

{user.name} — {user.email}

; } // ─── The rule ──────────────────────────────── // // API data is unknown until validated. // as User is a lie — it compiles but doesn't check. // isUser(data) actually verifies the shape. // // Validate once at the boundary, then trust the type. // Downstream components don't need null checks — // they receive a proper User | null from a parent // that already handled the null case. console.log(BadProfile, GoodProfile, isUser);
import { useEffect, useState } from 'react';

function isUser(x) {
return (
typeof x === 'object' &&
x !== null &&
'id' in x &&
'name' in x &&
'email' in x &&
typeof x.id === 'string' &&
typeof x.name === 'string' &&
typeof x.email === 'string'
);
}

function BadProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(/api/users/${userId}`)
.then(r => r.json())
.then((data) => setUser(data));
}, [userId]);
return React.createElement('p', null, user?.name ?? 'Loading...');
}

function GoodProfile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch(/api/users/${userId}`)
.then(r => r.json())
.then((data) => {
if (!isUser(data)) {
setError('Invalid response');
return;
}
setUser(data);
})
.catch(e => setError(e.message));
}, [userId]);

if (error) return React.createElement('p', null, 'Error: ', error);
if (!user) return React.createElement('p', null, 'Loading...');

return React.createElement('p', null, user.name, ' — ', user.email);
}

console.log(BadProfile, GoodProfile, isUser);

API data is unknown

await res.json() returns any (or unknown if you type it). Never trust the shape without validation.

as User is a lie

It compiles but doesn't check. If the server returns { error: '...' }, the cast succeeds and the crash happens later.

Type guard at the boundary

isUser(data) verifies the shape. If true, data is User. If false, handle the error explicitly.

Validate once

Don't validate in every component. Validate at the fetch wrapper. Everything downstream trusts the validated type.

Don't scatter ?. everywhere to avoid this crash. user?.name ?? '...' prevents the crash but hides the bug — you're rendering '...' instead of "the server returned an error." The right fix is to guard once, at the boundary, and let the component render the loading, error, or success state explicitly. Scattered optional chaining is a symptom, not a fix.

⚠️ Common mistakes

The crash patterns that cost real time.

1. Accessing properties on initial state

useState(null) then user.name. Fix: optional chaining or early return.

2. Calling .map() on possibly undefined

items.map(...) when items might be undefined. Fix: (items ?? []).map(...) or default the prop.

3. Accessing nested properties

user.address.city — address might be undefined. Fix: user?.address?.city or guard each level.

4. Using as on API data

data as User — trusts the shape. Fix: isUser(data) type guard.

5. Assuming children exists

children.map(...) — children might be undefined, string, etc. Fix: React.Children.toArray(children).

6. Not distinguishing loading vs error

user?.name ?? '...' hides both states. Fix: separate loading, error, success.

7. Forgetting ?.[ for arrays

items?.[0] — same as ?.prop but for indexed access. Fix: items?.[0]?.name.

8. Silencing with any

(user as any).name — hides the crash until you access a real nested prop. Fix: fix the null check.

❓ Frequently asked

How do I fix "Cannot read property of undefined" in React?

Three causes, three fixes. (1) Data not loaded yet — guard with optional chaining (obj?.prop), a loading state, or conditional render. (2) Missing prop — add a default in destructuring (function Component({ items = [] })). (3) Wrong shape — validate at the boundary with a type guard.

What is the difference between optional chaining and a null check?

Optional chaining (obj?.prop) returns undefined if obj is null or undefined, without throwing. A null check (if (obj) return obj.prop) does the same thing but with control flow. Use optional chaining in JSX for short access chains. Use null checks (or early returns) when you need to do more than access.

Why does React say the error is at line X when the bug is elsewhere?

React's error stack traces go up the component tree, not just the code. The line where the error was thrown is often deep inside a component's render function. Read the entire component stack — React shows the chain of components that led to the crash. The bug is usually in a parent that failed to pass data.

Should I use a library like lodash.get or just optional chaining?

Optional chaining (obj?.a?.b?.c) covers 95% of cases and has zero dependency. Use it unless you need lodash's get because: (1) you want a default value, (2) the path is dynamic (get(obj, someString)), (3) you need it in older environments.

🔗 Related

← All React problems
Problem solved · React