When building Lightning Web Components (LWC), one of the most important and often misunderstood topics is how to deal with asynchronous data fetching. Salesforce gives us multiple tools — namely @wire
, Promise.then()
, and async/await
. Each comes with its strengths and caveats. In this post, we’ll dissect when to use what and how to avoid common pitfalls.

Table of Contents
Understanding the Tools
Approach | Best For | Declarative | Can Use await ? | Built-in Retry Logic | Example Usage |
---|---|---|---|---|---|
@wire | Salesforce-provided wire adapters | ✅ Yes | ❌ No | ✅ Yes | getObjectInfo , getPicklistValues |
Promise.then() | Apex calls and JS promises | ❌ No, Imperative | ✅ Indirectly | ✅ Yes | getDataRecords() (Apex) |
async/await | Apex or JS utilities with promise support | ❌ No, Imperative | ✅ Yes | ✅ Yes | await getDataRecords() |
Why @wire
Isn’t Awaitable
You might be tempted to write this:
const objectInfo = await getObjectInfo({ objectApiName: 'Product2' });
But this results in:
LWC1503: "getObjectInfo" is a wire adapter and can only be used via the @wire decorator.
Wire adapters are reactive data providers. They are not traditional asynchronous functions and do not return Promises. You must either use them with the @wire
decorator or the rare imperative pattern (if supported).
What is Declarative Programming?
Declarative programming is about stating your intent — the “what”. You describe what data you want, and the Salesforce framework (in this case) handles the how — fetching, updating, re-running, etc.
How @wire
is Declarative:
- You declare a wire:
@wire(getObjectInfo, { objectApiName: 'Product2' }) objectInfo;
- This line says: “Hey Salesforce, I want the object metadata for Product2.” But we don’t control when or how often it runs — the framework does.
- If the
objectApiName
changes, LWC reactively re-fetches the data. - No manual
await
,.then()
, or error catch logic is needed up front.
It’s similar to how formulas in Excel update when referenced cells change. You define what you want once — the system keeps it fresh.
In Contrast: Imperative Code (async/await
or .then()
)
Imperative means telling the system how to do something, step-by-step.
const data = await getObjectInfo({ objectApiName: 'Product2' }); // Imperative
You’re telling the system: “Call this method, wait for the result, then continue.”
This is great when you need:
- Fine-grained control
- Error handling
- Sequencing (e.g., wait for recordTypeId, then fetch picklist values)
Declarative = less code, more magic. But it needs data dependencies to be right.
What are these Data Dependencies?
A data dependency tells Salesforce when to re-execute a wire function.
Example:
@track recordTypeId;
@wire(getPicklistValues, {
recordTypeId: '$recordTypeId',
fieldApiName: Status
}) picklistValues;
Here, recordTypeId
is a reactive variable(@track
). The $recordTypeId
syntax makes it a dependency. So when recordTypeId
changes, Salesforce re-runs the wire function automatically.
? If the dependency isn’t set correctly, the wire won’t run when it should. That’s a common source of bugs, especially when one wire depends on the output of another.
Common Pitfall: Chaining Wires Inside Promises
Here’s what not to do:
Promise.all([
getDataRecords(),
getObjectInfo({ objectApiName: PRODUCT_OBJECT }) // ❌ Invalid
])
getObjectInfo
must be wired. It won’t work in Promise.all()
.
The Right Way: Reactive Wiring
@track recordTypeId;
@wire(getObjectInfo, { objectApiName: PRODUCT_OBJECT })
handleObjectInfo({ data, error }) {
if (data) {
this.recordTypeId = data.defaultRecordTypeId;
} else {
console.error('Error fetching object info', error);
}
}
@wire(getPicklistValues, {
recordTypeId: '$recordTypeId',
fieldApiName: Status__c
})
handlePicklist({ data, error }) {
if (data) {
this.statusOptions = data.values;
} else {
console.error('Picklist fetch error', error);
}
}
Notice the $recordTypeId
. This creates a dependency so that getPicklistValues
only fires when the recordTypeId becomes available.
When to Use async/await
When you’re calling custom Apex methods or performing logic that needs controlled sequencing:
async connectedCallback() {
try {
const [data, userBU] = await Promise.all([
getDataRecords(),
getCurrentUserBU()
]);
this.processData(data, userBU);
} catch (err) {
console.error('Error loading metadata:', err);
}
}
processData(data, userBU) {..processing}
This makes error handling cleaner and the logic easier to follow.
Smart Combo: Wire + Imperative
Use @wire
for the reactive value, and trigger imperative logic based on that:
@wire(getObjectInfo, { objectApiName: PRODUCT_OBJECT })
handleObjectInfo({ data }) {
if (data) {
this.recordTypeId = data.defaultRecordTypeId;
this.loadPicklist();
}
}
loadPicklist() {
getPicklistValues({
recordTypeId: this.recordTypeId,
fieldApiName: Status
})
.then(result => {
this.statusOptions = result.values;
})
.catch(err => {
console.error('Picklist fetch error:', err);
});
}
Summary: When to Use What
Task | Best Approach |
---|---|
Get object metadata | @wire(getObjectInfo) |
Get picklist values | @wire(getPicklistValues) |
Call Apex method | async/await or Promise.then() |
Chain apex data-dependent logic | Wire + imperative method call |
Load multiple Apex sources at once | Promise.all with async/await |
Final Thoughts
In LWC, picking the right async strategy is more than just code style. It can mean the difference between a broken combobox and a smooth UX. Stick to @wire
when you want reactive behavior. Use async/await
for precision and control. Combine them smartly for real-world apps.
Have any questions or want a deep dive into picklist edge cases? Let us know in the comments.
Stay curious, stay declarative! ⚡