On This Page
This is the single most-hit runtime error in JavaScript, and it looks different depending on which engine printed it. If you landed here from a search, jump straight to The Fix — everything above it is context you can come back for.
1. The Error
In a browser (Chrome / Edge / any V8-based DevTools console)
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
at renderUser (app.js:14:24)
at app.js:22:1If the failing call is inside a promise chain instead of a synchronous call, Chrome prefixes it:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'map')
at renderList (app.js:9:29)The same access, three different engines
Given const user = undefined; user.name;, each shipping browser engine prints a genuinely different string:
# Chrome / Edge / Brave / any Chromium browser (V8)
TypeError: Cannot read properties of undefined (reading 'name')
# Firefox (SpiderMonkey)
TypeError: user is undefined
# Safari (JavaScriptCore)
TypeError: undefined is not an object (evaluating 'user.name')If you're on an older Chrome build (pre-Chrome 80 / V8 8.0, so nothing you'll see in a supported browser today) or reading an old Stack Overflow answer, you'll also see the previous V8 wording: TypeError: Cannot read property 'name' of undefined — singular "property", no "properties of". Same defect, older message. Treat every variant in this section as the same bug.
In Node.js
Node uses V8, so the message text matches Chrome exactly, but the frame and footer look different because there's no DevTools formatting:
/app/server.js:11
return req.body.user.name;
^
TypeError: Cannot read properties of undefined (reading 'name')
at getName (/app/server.js:11:25)
at /app/server.js:18:20
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/node_modules/express/lib/router/route.js:149:13)
Node.js v24.9.0The property block Node appends to some errors ({ code: '...' }) doesn't apply here — this is a plain TypeError, not a Node system error, so there's no .code.
2. How to Reproduce It
Browser reproduction
Save as repro.html and open with a local server (npx serve .) — file:// works too since nothing here touches the network, but keeping the habit of serving locally avoids surprises with fetch-based variants of this bug.
<!doctype html>
<html>
<body>
<ul id="list"></ul>
<script>
function renderList(users) {
// users is undefined on first paint — nothing has fetched yet
document.getElementById('list').innerHTML = users
.map((u) => `<li>${u.name}</li>`)
.join('');
}
let users; // no default value
renderList(users);
</script>
</body>
</html>Open DevTools and you'll see Cannot read properties of undefined (reading 'map') at the .map() call, because users never got a value before renderList ran.
Node.js reproduction
package.json:
{
"name": "repro",
"version": "1.0.0",
"type": "commonjs"
}repro.js (CommonJS, no dependencies):
function getName(req) {
return req.body.user.name;
}
// Simulates a request where the client sent a body,
// but didn't include a "user" object.
const fakeRequest = { body: { email: 'a@example.com' } };
console.log(getName(fakeRequest));Run it:
node repro.jsOutput:
/repro/repro.js:2
return req.body.user.name;
^
TypeError: Cannot read properties of undefined (reading 'name')
at getName (/repro/repro.js:2:24)
at Object.<anonymous> (/repro/repro.js:9:13)
Node.js v24.9.0Run it with node --stack-trace-limit=20 --enable-source-maps repro.js if the real failure is buried under a transpiled build — both flags are safe to leave on in development.
3. Version / Environment Behaviour Matrix
This is a language-level error, not a version-gated API, so it fires identically on every currently supported engine. What changes across versions is the wording and the tooling around it.
| Engine / runtime | Message for user.name where user is undefined | Notes |
|---|---|---|
| Chrome / Edge / Chromium (V8, current) | Cannot read properties of undefined (reading 'name') | Wording since V8 8.0 / Chrome 80 (Feb 2020); Baseline across all Chromium browsers. |
| Firefox (SpiderMonkey, current) | user is undefined | Uses the source identifier when one is available; falls back to undefined has no properties for a literal like undefined.name. |
| Safari (JavaScriptCore, current) | undefined is not an object (evaluating 'user.name') | Names the full failing expression rather than just the property. |
| Node.js 22 (Maintenance LTS, security support to 2027-04-30) | Same as Chrome (V8-based) | |
| Node.js 24 (Active LTS, security support to 2028-04-30) | Same as Chrome (V8-based) | Current recommendation for new production deployments. |
| Node.js 26 (Current since 2026-05-05, becomes Active LTS ~Oct 2026) | Same as Chrome (V8-based) | V8 14.x; no message-format change for this error. |
What changes next: nothing engine-side is scheduled to change this message again — the "properties" wording has been stable for six years. What is moving is Node's release cadence: starting with Node 27, every line follows an annual, calendar-aligned schedule with roughly 36 months from first Current release to end-of-life, replacing the old odd/even split. None of that affects how this error looks or when it fires.
4. Why It Happens — Surface Level
Something you expected to hold an object — an API response, a prop, a query result, an array element — instead holds undefined at the moment you read a property off it. The property access itself (.name, [0], .map()) is fine; the operand is the problem. The three most common sources: an async value read before the promise resolved, a nested path where one segment is optional (data.user.profile.name when profile can legitimately be absent), and a destructure or default parameter that silently produces undefined instead of throwing earlier, closer to the real mistake.
5. Why It Happens — Under the Hood
Every JS object access compiles down to [[Get]], the internal method the spec defines for reading a property off a value (ECMA-262 §7.3.2, GetV). The very first step of [[Get]] is RequireObjectCoercible, which checks whether the receiver can even be treated as an object — and by spec, only null and undefined fail that check. Every other value, including numbers and booleans, gets auto-boxed into a wrapper object first. null and undefined have no wrapper, so the engine throws immediately rather than boxing.
V8 implements this check inline as part of the property-load bytecode (GetProperty / the LdaNamedProperty handler in Ignition, or the equivalent inline cache check in Sparkplug/Maglev). When the receiver's type feedback says "this is usually an object" but the actual value at runtime is the undefined/null singleton, V8 skips straight to throwing rather than attempting an inline-cache lookup — there's no hidden class to consult, no shape to check, because undefined has no shape. That's also why this particular TypeError is cheap for V8 to detect and report precisely: it already knows which property name it was about to read, because the property name is baked into the bytecode operand, not computed. That's the whole reason the modern message can say "(reading 'name')" instead of just "cannot read property" — the interpreter has the identifier in hand at the throw site.
The three-engine wording split traces back to how each engine's parser attaches source-position metadata to a failing expression. V8 keeps the accessed property name on the throw site; SpiderMonkey instead tries to recover the source text of the base expression (user) from the AST and reports that identifier is undefined; JavaScriptCore reports the full evaluated expression string (user.name) because it defers message construction to a generic "evaluating expression" formatter shared by several TypeError variants. None of the three is "more correct" — they're just different design choices about what's most useful to show, made independently by three unrelated engine teams over the years.
6. The Fix
Quick fix — guard the specific access:
// Before
function renderList(users) {
return users.map((u) => u.name);
}
// After — optional chaining short-circuits to undefined instead of throwing
function renderList(users) {
return users?.map((u) => u.name) ?? [];
}?. stops evaluation and returns undefined the moment it hits a nullish value, so users?.map(...) never reaches the .map call when users is undefined or null. The ?? [] after it means callers still get an array back, not undefined, so a second-level .map() on the result doesn't just move the crash one line down.
Correct fix — stop the bad value at the boundary:
Optional chaining treats the symptom every time it's read. The better fix is making it impossible for users to be undefined at the call site in the first place:
// Node/Express example — validate the request body once, at the edge
import { z } from 'zod';
const BodySchema = z.object({
user: z.object({ name: z.string() }),
});
app.post('/greet', (req, res) => {
const parsed = BodySchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid request body' });
}
// parsed.data.user.name is guaranteed to exist and be a string here
res.json({ message: `Hello, ${parsed.data.user.name}` });
});// Browser example — give async state an explicit "not loaded yet" shape
function renderList(users) {
return users.map((u) => u.name); // safe: caller guarantees an array
}
let users = []; // never undefined — starts as an empty, valid state
fetchUsers().then((data) => {
users = data;
renderList(users);
});
renderList(users); // renders empty list, not a crash, before the fetch resolvesWhich one to reach for: use ?. at read sites you don't control (a third-party object, a deeply optional config path). Use boundary validation (schema parsing on the server, an explicit default/loading state on the client) for data that your code owns end to end — it turns a runtime crash into either a 400 response or a rendered empty state, both of which are recoverable.
7. Best Practices & The Better Design
The pattern that eliminates this class of bug rather than patching each instance: validate untrusted or asynchronous data exactly once, at the point it enters your code, and let the type system (or a runtime schema) guarantee everything downstream. Don't sprinkle ?. through twenty call sites that all read the same object — that's twenty places to forget it. Parse once with zod (or your schema library of choice) at the API boundary in Node, and model "not loaded yet" as an explicit state ({ status: 'loading' } / { status: 'ready', data }) in the browser instead of leaving a variable implicitly undefined until a promise resolves.
// TypeScript: make "hasn't loaded" a real, checked state
type UserListState =
| { status: 'loading' }
| { status: 'ready'; users: { name: string }[] }
| { status: 'error'; message: string };
function renderList(state: UserListState) {
if (state.status !== 'ready') return; // TS won't let you reach .map otherwise
return state.users.map((u) => u.name);
}With strict mode on, noUncheckedIndexedAccess enabled, and a discriminated union like this, state.users is only reachable inside the ready branch — the compiler refuses to compile the version that reads .map on a possibly-undefined value, catching the bug before the code ever runs.
This connects directly to two other high-volume topics worth reading next: Converting circular structure to JSON (a different failure mode from the same root cause — unvalidated data shape), and CORS/Failed to fetch (the other reason an async value ends up undefined — the request never completed at all).
8. How to Prevent It Long-Term
- Lint rule:
@typescript-eslint/no-unnecessary-conditionflags checks that TypeScript already proves impossible — and, run in the other direction withstrictNullCheckson, the compiler flags the access you forgot to guard, which is the failure mode that matters here. - Type-level guard:
tsc --noEmitwithstrict: trueandnoUncheckedIndexedAccess: true— the latter specifically makesarr[i]andobj[key]come back asT | undefinedinstead ofT, which surfaces exactly this bug at compile time for indexed access, not just named properties. - Runtime schema validation:
zod,valibot, orio-tsat every API boundary (incoming HTTP body, third-party API response,localStorageread) — parse untrusted data into a typed shape once instead of trusting it implicitly at every read site. - Tests: a unit test that calls your render/handler function with the "not yet loaded" or "field omitted" shape explicitly, not just the happy-path fixture. If your test fixtures always include every field, this bug ships regardless of how much you test.
- Runtime signal: track
TypeErrorvolume and message text in Sentry/RUM broken down by the property name being read ((reading 'x')) — a spike tied to one property name almost always maps to one recent deploy that changed that field's shape. - Logging: never
catch {}around code that produces this error hoping it "goes away" — log the error with its stack and the input that triggered it, or the next incident is the same bug with no trail.
9. Key Takeaways
Cannot read properties of undefined (reading 'x'),user is undefined, andundefined is not an object (evaluating 'user.x')are the same bug reported by V8, SpiderMonkey, and JavaScriptCore respectively — match on the pattern, not the exact string, when searching or de-duplicating issues.- The error is the JS spec's
RequireObjectCoerciblecheck failing — onlynullandundefinedtrigger it, because every other value gets auto-boxed before the property read. ?.and??are the right patch for data you don't own; they are not a substitute for validating data you do own at the boundary where it enters your system.- In TypeScript,
strictplusnoUncheckedIndexedAccessturns this from a runtime crash into a compile-time error for the indexed-access case — turn it on before you need it, not after an incident. - Model "hasn't loaded yet" as an explicit, checked state rather than an implicit
undefined— it's the single change that prevents the most common trigger of this error in UI code.
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.