Cannot read properties of undefined: find the real cause
Cannot read properties of undefined names the property you asked for, not the value that is missing. Here is how to find the real one, measured in Node.

Cannot read properties of undefined means you reached for a property on something that does not exist yet. JavaScript evaluated the expression left to right, found undefined partway along, and stopped. The value in parentheses at the end of the message is the property you asked for, not the thing that was missing.
That distinction is the whole debugging technique, and it is the part most explanations skip.
const user = {};
user.profile.name;
// TypeError: Cannot read properties of undefined (reading 'name')
name is in the message. name is not the problem. user.profile is the problem.
Every output on this page came from running the code in Node v24.14.1.
Read the message backwards
The rule is mechanical, so you can apply it without thinking about your program at all:
The property named in (reading 'x') is the one that failed. The thing immediately to its left is the value that is undefined.
Try it on a longer chain:
const a = { b: {} };
a.b.c.d;
// TypeError: Cannot read properties of undefined (reading 'd')
The message says d. So the undefined value is a.b.c. Not a, not a.b, and not d itself. You now know exactly which link in the chain to inspect, and you have not opened a debugger.
There is a second signal in the message. If the value were null rather than undefined, the wording changes:
const x = null;
x.y;
// TypeError: Cannot read properties of null (reading 'y')
undefined usually means something was never set, or a lookup came back empty. null usually means something set it deliberately. They point at different bugs, so it is worth reading which one you got.
Five places the undefined comes from
These are the sources that account for nearly every occurrence. Each one was run to confirm the message.
A nested object that is not there yet.
const user = {};
user.profile.name; // reading 'name'
An array index that does not exist. An empty array is not an error to index, it just gives back undefined, and the failure happens one step later.
const rows = [];
rows[0].id; // reading 'id'
A find() that matched nothing. This one is common in list-rendering code, because it works right up until the id you are looking for is absent.
const users = [{ id: 1 }];
users.find((u) => u.id === 2).id; // reading 'id'
A misspelled key. JavaScript does not warn you about a property that was never defined, it hands you undefined and moves on.
const res = { data: { items: [1] } };
res.Data.items; // reading 'items'
A function with no return value. Every function without an explicit return returns undefined, which then fails at the next dot.
function load() {}
load().length; // reading 'length'
The pattern behind all five is the same. Something produced undefined silently, and the error surfaced at the next property access rather than where the mistake was.
Find where it happened, not just what happened
The message tells you which value was undefined. The stack trace tells you where. Most people glance at the first line and give up, because the top frames often belong to a library.
Read down the trace until you hit the first file that is yours. That is the line to open. Frames above it are the library doing what you asked; frames below it are how you got there. The undefined value was almost certainly created in your frame or passed into it.
If the trace is all library frames, the value was passed in from somewhere else, and the useful question becomes which caller supplied it. Logging the argument at the top of your own function usually settles that in one run.
One habit makes this much faster. When you log a value to inspect it, log it as an object rather than on its own:
console.log({ user, profile: user.profile });
console.log(user.profile) prints undefined with no indication of what produced it. The object form prints the name alongside the value, so a screen of logs stays readable and you can see which of several candidates is the empty one.
The version that looks intermittent
The hardest variant of this error is the one that appears once and then disappears when you refresh. It is not random and it is not a caching problem. It is ordering.
Code that fetches data renders at least twice: once before the response arrives, and again after. On the first pass the variable holding the response is still empty, so any code that reaches into it crashes. On the second pass the data is there and everything works. If the first render happens too fast to see, you get a single error in the console and a page that looks fine.
const res = await fetch("/api/user");
const user = await res.json();
Until both of those lines have finished, user does not exist. Anything reading user.profile in the meantime hits exactly the error on this page.
The fix is to make the empty state a real case rather than an accident. Check for the data and render something else while it is missing, instead of reaching into it and hoping. The question to ask of any code that reads a fetched value is simply: what is this variable during the first pass, before the response exists?
Optional chaining, and the bug it hides
?. stops the crash. It checks whether the value on its left is null or undefined, and if so it gives back undefined instead of throwing.
const user = {};
user.profile?.name; // undefined, no error
Combine it with ?? when you have a sensible default:
const user = {};
user.profile?.name ?? "anonymous"; // "anonymous"
This is where most articles stop, and it is where you should be careful. ?. does not know the difference between data that is legitimately absent and a mistake you made. Here is the misspelled-key case again, with optional chaining added:
const u = { profile: { name: "Ada" } };
u.Profile?.name; // undefined
The name is right there. The capital P is a typo. With ?. in place, nothing throws, nothing logs, and the page quietly renders an empty field. The crash you removed was the only thing telling you the code was wrong.
So the honest rule is narrower than "use optional chaining":
- Use
?.where the value is genuinely optional, like a middle name or an avatar that some users have not set. - Do not use
?.to silence a value that is supposed to be there. Fix the reason it is missing, or check for it explicitly and handle the empty case on purpose.
A blanket ?. on every dot in a codebase converts loud bugs into silent wrong output, which is strictly harder to find.
Match the fix to the cause
Once you have identified which value is undefined, the fix follows from why:
| Why it is undefined | What to do |
|---|---|
| Data has not loaded yet | Render a loading state; do not read the field until it arrives |
| Array is empty | Check length, or handle the empty case before indexing |
find() matched nothing |
Treat "no match" as a real outcome, not an impossible one |
| Key is misspelled | Fix the spelling; do not reach for ?. |
| Function returns nothing | Add the missing return |
| Value is genuinely optional | ?. with a ?? default |
Notice that only one row in that table calls for ?.. The other five are bugs with a specific fix, and reaching for optional chaining on any of them replaces a crash with wrong output.
Stop it happening again
Three habits remove most of these before they run:
Initialise state as the shape it will eventually be. An array that starts as [] can be mapped over safely on the first render. One that starts as undefined cannot.
Return early instead of nesting deeper. If the data is not there, say so at the top of the function and stop. The rest of the body can then assume it exists.
Let the tooling read the chain for you. TypeScript refuses to compile user.profile.name when profile is optional, which turns this runtime crash into a red squiggle. It is the same class of win as catching a typo before it becomes a runtime error.
None of that helps in the ten seconds after the error appears. For that, use the rule at the top of this page: read the property in the parentheses, look immediately to its left, and inspect that value. It works every time because it is a description of how the engine evaluated your code, not a guess about your program.
Ready to practise this against real code that breaks on purpose? Start with the JavaScript track and get the failures in an editor where you can inspect them.
More from the blog

git cherry-pick: move one commit anywhere
Cherry-pick copies a commit onto your current branch. It creates a new commit with a new hash, so the original stays where it was.
Read more
CORS error: what the browser is actually blocking
A CORS error is the browser refusing a response the server already returned with a 200. Here is that proved with two real origins, and the fixes.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.