TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

TypeError: Cannot read properties of undefined (reading 'map') means you called .map() on something that does not exist. JavaScript evaluated the expression to the left of .map, got undefined, and stopped there.

The distinction that matters: the array is not empty, it is absent. An empty array maps fine and renders nothing. undefined has no properties at all, so the property lookup itself throws before your callback ever runs.

Older JavaScript engines print the same failure as Cannot read property 'map' of undefined. Same bug, older wording - everything on this page applies.

Below: the one-line fix, then 11 distinct causes with a reproduction and a fix for each, then a checklist for when your case matches none of them. Focused view of a computer screen displaying code and debug information.

ErrorTypeError: Cannot read properties of undefined (reading 'map')
Where it happensJavaScript - browser or Node.js, any framework. Most often React or Vue rendering a list from data that has not arrived, or a server response whose shape differs from what the code assumes.
What it meansThe value immediately to the left of .map is undefined, so there is no map property to read.

The Fast Fix

Give the value a real default and guard at the call site. In React, that means never initialising list state as undefined.

const [items, setItems] = useState([]); // NOT useState()

return (
  <ul>
    {(items ?? []).map(item => <li key={item.id}>{item.name}</li>)}
  </ul>
);

Outside React, the same shape works anywhere: (maybeList ?? []).map(fn).

One caution: items?.map(fn) stops the crash but evaluates to undefined, which is fine for JSX and breaks the moment you chain .join() or .length onto it. (items ?? []) always hands back an array, so prefer it when the result is used further.

If a default silences the error but you now render an empty list forever, the data never arrived in the shape you expected - keep reading, the cause is one of the 11 below.

What Is Actually Causing It

1. React state is undefined on the first render

Reproduce it

function UserList() {
  const [users, setUsers] = useState(); // no initial value

  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Why it happens — React renders the component before the effect runs, and again before the network response lands. On those renders users is still undefined, so users.map throws. The fetch is not slow - it is simply not first.

The fix

function UserList() {
  const [users, setUsers] = useState([]); // seed with an array
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(setUsers)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Spinner />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Changed: useState([]) instead of useState(), plus an explicit loading branch so an empty list and a pending list are distinguishable.

Confirm it worked — Add console.log('render', users) above the return. The first log must print [], never undefined. Throttle the network to Slow 3G in DevTools and confirm the page still renders.


2. The array sits one level deeper in the response payload

Reproduce it

// GET /api/users responds with: { "data": { "users": [ ... ] } }
const res = await fetch('/api/users');
const payload = await res.json();

const names = payload.users.map(u => u.name); // payload.users is undefined

Why it happenspayload is a valid object, but it has no users key - the array lives at payload.data.users. Reading a missing key yields undefined, and the crash happens on the next property access, not on the wrong key itself.

The fix

const payload = await res.json();
const names = (payload.data?.users ?? []).map(u => u.name);

Changed: the correct path payload.data.users, with optional chaining so an envelope change fails loudly at your validation layer rather than mid-render.

Confirm it workedconsole.log(JSON.stringify(payload, null, 2)) once, or open the Network tab and read the raw response body. The path you log must match the path you index.


3. A promise was never awaited, so you read a property off the Promise

Reproduce it

function getUsers() {
  return fetch('/api/users').then(r => r.json()); // resolves to { users: [...] }
}

const result = getUsers();     // a Promise, not the payload
result.users.map(u => u.name); // result.users is undefined

Why it happensgetUsers() returns a Promise object. A Promise has then, catch and finally - it does not have users. So result.users is undefined and .map throws. This bites hardest when you refactor a sync function to async and miss one call site.

The fix

const result = await getUsers();
const names = result.users.map(u => u.name);

Changed: added await (the enclosing function must be async). Inside React, do the await inside the effect and store the result in state.

Confirm it workedconsole.log(result) - if it prints Promise { <pending> } you are missing an await. Enabling the ESLint rule no-floating-promises (TypeScript ESLint) catches the whole class of these.


4. The server returned an error body instead of the array

Reproduce it

const res = await fetch('/api/users'); // 401 Unauthorized
const body = await res.json();         // { "error": "token expired" }

body.data.map(u => u.name);            // body.data is undefined

Why it happensfetch does not reject on 4xx or 5xx - it resolves normally and hands you whatever JSON the error handler produced. Your success-path code then indexes a shape that only exists on 200.

The fix

const res = await fetch('/api/users');
if (!res.ok) {
  throw new Error(`GET /api/users failed: ${res.status}`);
}
const body = await res.json();
const names = body.data.map(u => u.name);

Changed: an explicit res.ok check, so a failed request surfaces as an auth or server error instead of a TypeError three lines later.

Confirm it worked — Log res.status next to the crash, or filter the Network tab by status. If the failing request is not 200, the bug is upstream of the .map.


5. A prop was misspelled or never passed by the parent

Reproduce it

// parent
<TodoList todos={todos} />

// child
function TodoList({ items }) {
  return <ul>{items.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

Why it happens — The parent passes todos, the child destructures items. A missing key destructures to undefined silently - no warning, no crash at the boundary - and the failure surfaces at the first property access inside the child.

The fix

// child - names match, plus a default for the case the parent omits it
function TodoList({ todos = [] }) {
  return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

Changed: the destructured name now matches the passed prop, and = [] covers a parent that renders <TodoList /> with no props at all.

Confirm it worked — Select the component in React DevTools and read its props panel, or add console.log(Object.keys(props)) at the top of the component. TypeScript props typing turns this into a compile error instead.


6. A variable is only assigned inside a branch that did not run

Reproduce it

let rows;

try {
  rows = JSON.parse(raw).rows;
} catch (err) {
  console.error('bad json', err); // swallowed - rows stays undefined
}

return rows.map(r => r.id);

Why it happenslet rows; initialises to undefined. When the try block throws - malformed JSON, a missing file, a non-200 upstream - the catch logs and execution continues with rows still undefined. The crash lands several lines away from the real failure.

The fix

let rows = []; // safe default

try {
  rows = JSON.parse(raw).rows ?? [];
} catch (err) {
  throw new Error(`could not parse payload: ${err.message}`);
}

return rows.map(r => r.id);

Changed: declared with a default, and the catch now rethrows with context instead of swallowing. Pick one - a real default or a real throw - never a silent log.

Confirm it worked — Feed the function deliberately malformed input ('{'). It should either return [] or throw your message with the parse detail, and never reach the .map with undefined.


More causes (5 remaining)

7. An optional nested field is missing on some records only

Reproduce it

// works against seed data, crashes in production
const labels = users.map(u => u.profile.tags.map(t => t.label));

Why it happensusers is a fine array and the outer .map runs. But some users have never added tags, so u.profile.tags is undefined for those records and the inner .map throws. This is the intermittent variant - it passes CI and fails on one customer's account.

The fix

const labels = users.map(u => (u.profile?.tags ?? []).map(t => t.label));

Changed: optional chaining on profile and a ?? [] fallback on tags, so records without tags contribute an empty array instead of exploding.

Confirm it worked — Run users.filter(u => !u.profile?.tags).length against the real dataset. A non-zero count is your offending records - inspect one of them directly.


8. A helper function with a braced arrow body forgets to return

Reproduce it

const getTags = (post) => {
  post.tags.filter(Boolean); // computed, then discarded
};

getTags(post).map(t => t.id); // getTags(...) is undefined

Why it happens — An arrow function with a block body returns undefined unless you write return. The refactor from (post) => post.tags.filter(Boolean) to a braced body to add one log line is the usual origin.

The fix

const getTags = (post) => {
  return post.tags.filter(Boolean); // explicit return
};

getTags(post).map(t => t.id);

Changed: added return. Dropping the braces entirely - const getTags = post => post.tags.filter(Boolean); - makes the mistake impossible.

Confirm it workedconsole.log(getTags(post)) prints undefined before the fix and an array after. Enable the ESLint rules consistent-return and array-callback-return to catch these at lint time.


9. Chained onto a method that returns undefined, such as forEach

Reproduce it

const ids = rows
  .forEach(r => r.trim()) // forEach returns undefined
  .map(r => r.id);

Why it happensforEach exists for side effects and always returns undefined. So does a push result used as an array (it returns the new length), and so does any callback you meant to be a transform but wrote as a loop. The chain breaks at the next link.

The fix

const ids = rows
  .map(r => r.trim()) // map returns a new array
  .map(r => r.id);

Changed: forEach to map. If you genuinely need side effects, run them in their own statement and keep the chain on array-returning methods (map, filter, slice, flatMap, concat).

Confirm it worked — Log the intermediate: console.log(rows.forEach(r => r)) prints undefined, confirming the break point. Split any long chain into named intermediates until the undefined one is obvious.


10. A key typo or a dynamic key that does not exist

Reproduce it

const config = require('./config.json'); // { "routes": [ ... ] }

config.route.map(r => r.path); // 'route' is not 'routes'

Why it happens — Object property access on a missing key returns undefined rather than throwing. Singular/plural slips, camelCase versus snake_case mismatches across a language boundary, and computed keys like data[section] where section came from a URL param all land here.

The fix

const config = require('./config.json');

const list = config.routes;
if (!Array.isArray(list)) {
  throw new Error(`config.routes missing; keys: ${Object.keys(config)}`);
}
list.map(r => r.path);

Changed: the corrected key plus an Array.isArray assertion that names the available keys when it fails - the next typo diagnoses itself.

Confirm it workedconsole.log(Object.keys(config)) and compare against the key you typed. For computed access, log the key variable itself - it is often an empty string or undefined.


11. A destructuring default does not reach the nested key

Reproduce it

function renderTable({ config = {} }) {
  return config.columns.map(c => c.title); // config.columns is undefined
}

renderTable({});             // crashes
renderTable({ config: {} }); // crashes

Why it happens — The default = {} fills in the outer object only - it does not invent a columns key inside it. Defaults also fire for undefined only, so renderTable({ config: null }) skips the default entirely and fails with Cannot read properties of null instead.

The fix

function renderTable({ config }) {
  const { columns = [] } = config ?? {};
  return columns.map(c => c.title);
}

Changed: the default moved to the level you actually read, and ?? {} covers null as well as undefined.

Confirm it worked — Write three one-line unit tests: renderTable({}), renderTable({ config: {} }), renderTable({ config: null }). All three must return [] rather than throw.

Vivid, blurred close-up of colorful code on a screen, representing web development and programming. ## None of Those? Narrow It Down
  1. Read the property name in the message. (reading 'map') tells you the runtime got to the dot and failed - so the undefined value is the expression immediately to the left of .map, not the array elements and not the callback. Name that expression before doing anything else.
  2. Log that exact expression one line above the crash: console.log('LEFT:', typeof x, x). undefined confirms the diagnosis; an object means the crash is on a different .map in the same line and you should split the chain into separate statements.
  3. Check whether it fails on every render or only the first. First-render-only points at cause 1 (async state). Every time points at a shape or naming problem - causes 2, 5, 10, 11.
  4. Open the Network tab, find the request, and read the status plus the raw body. A non-200 status rules in cause 4; a 200 with a different nesting than your code assumes rules in cause 2.
  5. Check whether it fails for all records or some. Add data.filter(d => !d.thatField).length - a non-zero count rules in cause 7 (optional field) and rules out a global shape mismatch.
  6. Search the codebase for every assignment to the identifier. If any code path leaves it unassigned - a catch block, an early if, a branch behind a feature flag - that is cause 6.
  7. If the value comes from a function you wrote, log the function's return directly: console.log(theHelper(input)). undefined rules in cause 8 (missing return) or cause 9 (chained onto forEach).
  8. Still unmatched? Reproduce in isolation: call the failing function from a Node REPL or a unit test with {}, undefined, and the real payload copied from the Network tab. The input that reproduces it identifies the cause.

Why This Error Exists At All

JavaScript has two distinct nothings. undefined means no value was ever supplied - a missing object key, an unpassed argument, a function that fell off its end. null means a value was deliberately set to nothing. Neither is an object, and property access is defined only on objects (plus the primitives that box into wrappers). So undefined.map is not a missing method - it is a property read on something that has no property table at all. The engine cannot even get far enough to tell you map does not exist.

That design choice is why the runtime cannot help you more. A language could have chosen null-safe navigation by default, returning undefined for every access on nothing. JavaScript did not, because silently propagating nothing turns a small bug into a wrong result that surfaces three modules away. Throwing at the first illegal access keeps the stack trace close to the real mistake. The modern message format is the concession: (reading 'map') names the property you tried to reach, which is how you locate the undefined value - it is always the expression to that property's left. This is the single most useful habit to take from this page. Cannot read properties of undefined (reading 'length'), (reading 'id'), (reading 'toString') are the same error with a different property, and they are all read the same way.

Once you see the family, the fix strategies collapse into two, and picking between them is the actual engineering decision. Defaulting - ?? [], useState([]), = [] at destructuring - says absent is a legitimate state and empty is the right behaviour. Asserting - if (!res.ok) throw, Array.isArray(x) || throw, a schema parse at the boundary - says absent means something upstream is broken and I want to know now. Sprinkling optional chaining everywhere is neither: it converts loud failures into silently empty screens, and you will spend an afternoon wondering why the list renders nothing. Default where absence is expected, assert where it is not, and do the assertion once at the boundary where data enters your code rather than at every point of use. Software developer analyzing code on a tablet in a modern office workspace.

Stop It From Coming Back

  • Type the boundary with TypeScript and turn on strict. strictNullChecks makes possibly 'undefined' a compile error at exactly the .map call sites listed above; add noUncheckedIndexedAccess so arr[0].map(...) is also flagged.
  • Validate API responses at the edge with a runtime schema library (Zod, Valibot, io-ts). schema.parse(await res.json()) fails once, at the fetch, with a message naming the wrong path - instead of failing later inside a render.
  • Enable the ESLint rules array-callback-return and consistent-return to catch the missing-return helper, and @typescript-eslint/no-floating-promises to catch the unawaited promise.
  • Never initialise list state as bare useState() or let rows;. Seed with [] and track loading separately, so an empty result and an unfetched result are two different states in your code and on screen.
  • Check res.ok (or use a client like Axios or Ky that rejects on non-2xx) before touching the parsed body - it eliminates the entire error-body class of this crash.
  • Add unit tests that feed the component or function {}, undefined, null, and an empty array. Those four inputs cover most of the causes on this page and cost about six lines each.

Errors You Will Probably Hit Next

  • TypeError: Cannot read properties of null (reading 'map') - the value was explicitly null, so ?? []helps but a= [] destructuring default will not.
  • TypeError: users.map is not a function - the value exists but is an object, a string, or a NodeList, not an array. Usually the nesting problem from cause 2, one level off in the other direction.
  • TypeError: Cannot destructure property 'items' of 'data' as it is undefined - same undefined value, caught by destructuring instead of by a property read.

Read the property in parentheses, then look immediately to its left - that expression is your undefined value, every single time. From there the question is only whether absence is expected or a bug: default it with ?? [] if empty is a valid state, assert and throw at the data boundary if it is not. Optional chaining everywhere is not a third answer, it is how an obvious crash becomes a blank screen you debug next week.

댓글

이 블로그의 인기 게시물

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes

npm ERR! code ERESOLVE unable to resolve dependency tree: 9 Causes and Fixes