Cannot read properties of undefined (reading 'X') — in LWC component lifecycle
Plain JavaScript TypeError, but in LWC it almost always means a `@wire` result was read in `connectedCallback` or `renderedCallback` before the wire actually returned data. The fix is to defer reads until you have data, or to use the wire's loading state explicitly.
Also seen asCannot read properties of undefined·Cannot read property of undefined·TypeError: Cannot read properties of undefined·LWC undefined
A user opens an Account record page. Your custom LWC renders for a moment, then disappears with a console error: TypeError: Cannot read properties of undefined (reading 'fields'). The component worked in the developer sandbox. In production it fails the first time the @wire returns. The bug is timing: your code read data that wasn't there yet.
The lifecycle and the timing of wires
LWC components have a defined lifecycle. The hooks fire in order: constructor, connectedCallback, renderedCallback, disconnectedCallback. The @wire decorator runs separately; the platform fetches the wired resource and calls your wire handler when the data arrives.
The key fact: the wire handler runs after connectedCallback. If your code reads the wired data in connectedCallback, the data is still undefined. Any property access on it throws TypeError.
A second key fact: the wire calls back twice. The first call passes { data: undefined, error: undefined } to signal the wire is fetching. The second call (and subsequent calls on re-renders) passes either { data: <result>, error: undefined } on success or { data: undefined, error: <error> } on failure. If your code reads the wired property after the first call but before the second, it sees undefined.
The broken example
A component that displays an account's industry, written assuming the wire data is available immediately:
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class AccountIndustry extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: [INDUSTRY_FIELD] })
account;
connectedCallback() {
// Throws: Cannot read properties of undefined (reading 'fields')
console.log('Industry is:', this.account.data.fields.Industry.value);
}
}
The component compiles cleanly. On render, connectedCallback runs before the wire resolves. this.account exists (the framework creates the wire object) but this.account.data is undefined. Reading data.fields.Industry.value throws.
The fix: guard the read with the loading state
Three patterns work; pick based on the component's design.
Pattern 1: read in the template, with lwc:if guards. The template re-renders when the wire updates. Use the data only when it exists:
<template>
<template lwc:if={account.data}>
<p>Industry: {industry}</p>
</template>
<template lwc:elseif={account.error}>
<p>Error: {account.error.message}</p>
</template>
<template lwc:else>
<lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
</template>
get industry() {
return this.account?.data?.fields?.Industry?.value;
}
The optional chaining (?.) gracefully handles every undefined level. The template only renders the data branch when account.data is defined.
Pattern 2: use a wire handler function instead of a property. The handler fires when the wire resolves, with the data already available:
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class AccountIndustry extends LightningElement {
@api recordId;
industry;
error;
@wire(getRecord, { recordId: '$recordId', fields: [INDUSTRY_FIELD] })
wiredAccount({ data, error }) {
if (data) {
this.industry = data.fields.Industry.value;
this.error = undefined;
} else if (error) {
this.error = error;
this.industry = undefined;
}
}
}
The handler runs only when the wire has data or an error. Reading inside it is safe.
Pattern 3: imperative call from connectedCallback. If you need the data in a specific lifecycle moment, call the imperative version of the wire:
import { getRecord } from 'lightning/uiRecordApi';
// ...
async connectedCallback() {
try {
const record = await getRecord({ recordId: this.recordId, fields: [INDUSTRY_FIELD] });
this.industry = record.fields.Industry.value;
} catch (e) {
this.error = e;
}
}
The await blocks until the data arrives. Use this when the wire-pattern fragility is more pain than the imperative pattern's verbosity.
The fixed example, with loading states
A complete component that handles loading, success, and error states cleanly:
<template>
<lightning-card title="Industry">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading industry"></lightning-spinner>
</template>
<template lwc:elseif={error}>
<p class="slds-text-color_error slds-p-around_small">
Couldn't load industry: {error.message}
</p>
</template>
<template lwc:else>
<p class="slds-p-around_small">
<strong>Industry:</strong> {industry}
</p>
</template>
</lightning-card>
</template>
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class AccountIndustry extends LightningElement {
@api recordId;
industry;
error;
isLoading = true;
@wire(getRecord, { recordId: '$recordId', fields: [INDUSTRY_FIELD] })
wiredAccount({ data, error }) {
this.isLoading = false;
if (data) {
this.industry = data.fields.Industry.value;
this.error = undefined;
} else if (error) {
this.error = error;
this.industry = undefined;
}
}
}
Three explicit states: loading, error, ready. The template chooses which to render. The JavaScript never reads from undefined.
Optional chaining as a fallback safety net
Modern JavaScript's optional chaining (?.) makes the loose-read pattern safer:
get industryName() {
return this.account?.data?.fields?.Industry?.value ?? 'Unknown';
}
The ?? 'Unknown' provides a sensible default when any level is undefined. The full expression doesn't throw; it just returns the default.
Use optional chaining wherever you read a chained property that might be undefined. It's especially useful in computed getters that the template references.
Why the wire fires twice (and sometimes more)
The wire fires for several reasons:
- The first invocation, with
dataanderrorboth undefined, signals the wire is fetching. - The data-arrival invocation, with
datapopulated. - Re-fetches when the wire's parameters change (e.g.,
recordIdchanges). - Cache invalidation by other components (Lightning Data Service refreshes its cache when relevant records are updated).
- Manual refresh via
refreshApex().
Each invocation calls your wire handler again. Your handler must be idempotent: running it with the same data multiple times shouldn't have visible side effects.
A subtle case: parameters that aren't ready yet
If your wire references a property that hasn't been set, the wire doesn't fire at all (waiting state). For example:
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
wiredAccount({ data, error }) { ... }
If this.recordId is undefined (e.g., the component is on a page that doesn't have a record context), the wire stays in waiting and your handler never gets called. The data stays undefined indefinitely.
The fix: guard your component's UI to handle the "no record id" case explicitly. Don't assume the wire will eventually fire.
When the error is in a deeper property chain
If your data shape is deeply nested (record.fields.SomeRelationship.value.Name), the optional chaining helps but doesn't tell you which level is undefined. For debugging, log the full data once:
@wire(getRecord, { ... })
wiredAccount({ data, error }) {
if (data) console.log('Wire data:', JSON.parse(JSON.stringify(data)));
// ...
}
The JSON.parse(JSON.stringify(...)) produces a plain object that's easier to inspect in the browser console.
For complex shapes, define a TypeScript-like type comment at the top of your file so future maintainers know what to expect:
// Wire data shape:
// {
// data: { fields: { Industry: { value: 'Tech' }, Name: { value: 'Acme' } } },
// error: undefined
// }
The comment isn't enforced, but it documents the contract and saves the next developer fifteen minutes of inspection.
The connectedCallback vs renderedCallback distinction
Two lifecycle hooks that look similar:
connectedCallbackfires once when the component is inserted into the DOM. Wire data isn't available here.renderedCallbackfires after every render. Wire data is sometimes available, but the hook runs many times, so use it carefully.
For one-time setup that depends on wire data, the right pattern is a wire handler function, not a lifecycle hook. The lifecycle hooks predate wire and don't compose well with it.
A subtle case in Lightning Out
When an LWC is embedded outside Salesforce (Lightning Out), the wire infrastructure has slightly different behavior. The data context isn't always set up the same way. Test specifically in the embedded environment if your component will be used there.
Test patterns
A Jest test that confirms the component handles the loading state:
import { createElement } from 'lwc';
import AccountIndustry from 'c/accountIndustry';
describe('account-industry', () => {
it('shows loading initially', () => {
const element = createElement('c-account-industry', { is: AccountIndustry });
element.recordId = '001xx000003DGb0';
document.body.appendChild(element);
const spinner = element.shadowRoot.querySelector('lightning-spinner');
expect(spinner).not.toBeNull();
});
});
For a fuller test that exercises wire data delivery, use @salesforce/wire-service-jest-util to simulate wire emissions. The library is part of the standard Salesforce DX testing toolkit.
What about non-wire data sources
The same pattern applies when reading from any asynchronously-arriving data source: imperative Apex calls, fetch promises, message channel subscriptions. The receiver code shouldn't assume the data is present until the async call has resolved.
For imperative Apex:
async loadData() {
try {
const result = await someApexMethod({ id: this.recordId });
this.data = result;
} catch (e) {
this.error = e;
} finally {
this.isLoading = false;
}
}
The pattern mirrors the wire-handler pattern. The component starts in isLoading = true, the async call resolves either to data or to error, and the template renders based on the explicit state.
A common debugging anti-pattern
When a component throws Cannot read properties of undefined, the temptation is to add a single guard at the offending line: if (this.account.data) { ... }. The fix is local but doesn't address the structural issue. Three lines later, another property access without a guard throws the same error.
Better: refactor to the three-state pattern (loading, error, ready) and route every data access through the ready branch. A few minutes of structure pays off in fewer downstream incidents.
Performance: avoid heavy work in connectedCallback
A subtle related issue: doing heavy synchronous work in connectedCallback delays the wire from firing. The browser is single-threaded; the wire's data-arrival callback can't run until your callback returns. Heavy work in connectedCallback makes the component look frozen.
Keep connectedCallback short. Defer heavy work to a setTimeout(..., 0) or to a wire handler. The component renders the loading state immediately; the work runs in the background.
The race with parent-passed props
If your component receives data via @api props from a parent, those props arrive synchronously: by the time connectedCallback runs, the props are set. This is different from wire data, which arrives asynchronously.
A common mistake: treating wire data the same way as a prop, expecting it to be available in connectedCallback. The mental model needs to distinguish the two:
@apiprops: synchronous, available in all lifecycle hooks.@wiredata: asynchronous, available only after the wire fires.
Code that mixes the two needs to read @api props in connectedCallback and @wire data in wire handlers. The split is a hard rule.
Refactor candidate: extract data logic into a service
For components that mix multiple data sources (one wire for the record, another wire for related records, an imperative Apex call for some derived value), the lifecycle complexity multiplies. Each source has its own readiness state.
Extract a service class that owns the data logic. The component asks the service for "the rendered data" and gets a single ready/loading/error tuple. The component's template branches on that tuple. The lifecycle complexity lives in one place.
The pattern requires a bit more code, but it scales much better than juggling many wire handlers in the same component.
A subtle case: refreshApex and stale data
Calling refreshApex(this.wiredAccount) triggers the wire to re-fetch. During the re-fetch, the wire briefly returns data: undefined again. If your component re-reads this.wiredAccount.data.fields during the refresh, you can hit the same TypeError.
The fix is the same: guard the reads. The pattern that works once works during refresh too.
Why TypeScript-style annotations help
LWC source can be plain JavaScript or annotated with type-aware tooling like JSDoc. Adding type comments to wire results makes the data shape explicit:
/**
* @typedef {{
* data: { fields: { Industry: { value: string }, Name: { value: string } } } | undefined,
* error: Error | undefined
* }} WiredAccount
*/
The annotation isn't enforced by the LWC compiler, but modern IDEs use JSDoc to provide autocomplete and warnings. The annotation documents the contract for the next developer and surfaces type mismatches at edit time.
For teams that want full type safety, LWC supports TypeScript directly in modern API versions. The compiler enforces the types and catches "property might be undefined" issues at compile time, eliminating the runtime TypeError entirely.
Further reading from Salesforce
- Lightning Web Components Developer Guide: Lifecycle Hooks
- LWC Reference: @wire Decorator
- LWC Reference: Lightning Data Service
- LWC Reference: Conditional Rendering
- Trailhead: Build Lightning Web Components
Related dictionary terms
Share this fix
Related Lightning · LWC errors
@wire(getRecord, ...) requires the fields property
Lightning · LWCYou used `getRecord` from `lightning/uiRecordApi` without specifying which fields to fetch. LDS doesn't auto-fetch all fields like an old-sc…
Action failed: c:myComponent$controller$myAction [<JS error message>]
Lightning · LWCAn Aura component's client-side JavaScript controller threw an exception. The error message is wrapped in `Action failed: c:component$contro…
Cannot find component: c:myComponent
Lightning · LWCThe Lightning runtime tried to instantiate a component by name and the platform doesn't have it — the bundle didn't deploy, the namespace pr…
Lightning component cannot be created: c:myComponent
Lightning · LWCThe framework couldn't instantiate a Lightning component at runtime — usually a missing dependency, a permission gap, or a static-resource l…
LWC1009: <template> directives are not allowed at the top level
Lightning · LWCYou put `if:true`, `for:each`, `iterator`, or another LWC template directive on the *root* `<template>` of an LWC HTML file. Directives belo…