Part 84: Retrieving Data from Apex in LWC
At some point, every Lightning Web Component needs data that lives in Salesforce. You have already seen the @wire decorator in Part 78 and learned about Lightning Data Service in Part 83, which is great for simple record operations. But real-world applications need more. You need to run SOQL queries with complex filters, perform DML across multiple objects, call out to external services, or execute business logic that only exists in Apex. That means your LWC needs to talk directly to an Apex controller.
Salesforce gives you two ways to do this: the wire service and imperative calls. Both connect your client-side JavaScript to server-side Apex methods, but they behave differently, and choosing the right one matters. The wire service is declarative and reactive — you tell the framework what data you want, and it handles fetching, caching, and re-fetching automatically. Imperative calls are manual — you decide exactly when the call happens, and you handle the response yourself.
This post covers:
- How to call Apex Class Methods Imperatively from an LWC — Full control over when and how the call fires.
- How to call Apex Class Methods via the wire service from an LWC — Reactive, declarative data fetching.
- How to use the refreshApex method — Forcing stale wired data to re-fetch.
- How to create Salesforce objects in JSON and send them to Apex via your LWC — Passing SObject data from the client to the server.
- JS exception handling for Apex calls — Catching and handling errors properly on both sides.
Let’s start with imperative calls, since they are the most straightforward to understand.
How to Call Apex Class Methods Imperatively from an LWC
An imperative Apex call is exactly what it sounds like — you call the method yourself, at the exact moment you want it to run. This is the approach you reach for when the call should happen in response to a user action like a button click, when you need to chain multiple calls together, or when you want full control over the timing.
The Apex Controller
Every Apex method you want to call from an LWC must follow two rules: it must be static, and it must be decorated with @AuraEnabled. The cacheable parameter is optional and we will come back to it shortly.
public with sharing class AccountController {
@AuraEnabled
public static List<Account> getAccounts(String searchTerm) {
String wildcard = '%' + searchTerm + '%';
return [
SELECT Id, Name, Industry, Phone
FROM Account
WHERE Name LIKE :wildcard
ORDER BY Name
LIMIT 50
];
}
}
A few things to note here. The method is public, static, and has @AuraEnabled on it. The with sharing keyword ensures that the query respects the running user’s record-level access. Always use with sharing unless you have a specific reason not to — it is a security best practice.
The LWC JavaScript
On the LWC side, you import the Apex method using a special module path. The format is @salesforce/apex/ClassName.methodName. Once imported, the method is a function that returns a Promise.
import { LightningElement } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountSearch extends LightningElement {
searchTerm = '';
accounts = [];
error;
isLoading = false;
handleSearchChange(event) {
this.searchTerm = event.target.value;
}
async handleSearch() {
if (!this.searchTerm) {
return;
}
this.isLoading = true;
this.error = undefined;
try {
this.accounts = await getAccounts({ searchTerm: this.searchTerm });
} catch (error) {
this.accounts = [];
this.error = error;
} finally {
this.isLoading = false;
}
}
}
The key detail is how you pass parameters. You pass a single object where each key matches the parameter name in the Apex method. So getAccounts({ searchTerm: this.searchTerm }) maps to the String searchTerm parameter in the Apex class. The method returns a Promise, so you can use async/await or .then()/.catch() — either works fine. I prefer async/await because it reads more naturally, especially when you need to chain calls.
The LWC HTML
<template>
<lightning-card title="Account Search">
<div class="slds-p-around_medium">
<lightning-input
label="Search Accounts"
value={searchTerm}
onchange={handleSearchChange}>
</lightning-input>
<lightning-button
label="Search"
variant="brand"
onclick={handleSearch}
class="slds-m-top_small">
</lightning-button>
</div>
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
<template if:true={accounts.length}>
<lightning-datatable
key-field="Id"
data={accounts}
columns={columns}
hide-checkbox-column>
</lightning-datatable>
</template>
<template if:true={error}>
<p class="slds-text-color_error slds-p-around_medium">
Something went wrong: {error.body.message}
</p>
</template>
</lightning-card>
</template>
Notice that the Apex call only fires when the user clicks the Search button. That is the whole point of imperative calls — you control the timing. The framework is not managing this for you.
How to Call Apex Class Methods via the Wire Service from an LWC
The wire service is the declarative, reactive alternative. Instead of calling a method manually, you wire a property or function to an Apex method, and the framework handles the rest. Whenever the input parameters change, the framework re-invokes the Apex method automatically. If you covered Part 78 on the @wire decorator, you have already seen the basic syntax. Now we are applying it specifically to Apex.
The Apex Controller
There is one critical difference when you want to use the wire service: the Apex method must have cacheable=true in its @AuraEnabled annotation. This tells the framework that the method is safe to cache — it only reads data, it does not modify anything.
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContactsByAccount(Id accountId) {
return [
SELECT Id, FirstName, LastName, Email, Phone
FROM Contact
WHERE AccountId = :accountId
ORDER BY LastName
];
}
}
The cacheable=true annotation is not optional here. If you try to wire to a method without it, you will get a runtime error. This also means that wired methods cannot perform DML operations — no inserts, updates, or deletes. If you need to modify data, use an imperative call.
Wiring to a Property
The simplest approach is wiring to a property. The framework assigns an object with data and error keys to the property automatically.
import { LightningElement, api, wire } from 'lwc';
import getContactsByAccount from '@salesforce/apex/ContactController.getContactsByAccount';
export default class ContactList extends LightningElement {
@api recordId;
@wire(getContactsByAccount, { accountId: '$recordId' })
contacts;
get hasContacts() {
return this.contacts.data && this.contacts.data.length > 0;
}
get contactList() {
return this.contacts.data || [];
}
get errorMessage() {
return this.contacts.error ? this.contacts.error.body.message : '';
}
}
The $recordId syntax with the dollar sign is reactive binding. It tells the wire service to watch this.recordId and re-invoke the Apex method whenever that value changes. So if the parent component passes a new recordId, the contact list automatically refreshes. No manual calls, no event listeners, no imperative logic. The framework handles it all.
Wiring to a Function
If you need more control over what happens when data arrives, you can wire to a function instead. The function receives the same { data, error } object but lets you process or transform the data before storing it.
import { LightningElement, api, wire } from 'lwc';
import getContactsByAccount from '@salesforce/apex/ContactController.getContactsByAccount';
export default class ContactListProcessed extends LightningElement {
@api recordId;
contacts = [];
error;
totalContacts = 0;
@wire(getContactsByAccount, { accountId: '$recordId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data.map(contact => ({
...contact,
fullName: `${contact.FirstName || ''} ${contact.LastName}`.trim()
}));
this.totalContacts = data.length;
this.error = undefined;
} else if (error) {
this.contacts = [];
this.error = error;
this.totalContacts = 0;
}
}
}
The function approach is useful when you need to compute derived values, merge data from multiple sources, or reshape the response before it hits the template. It gives you a middle ground between the fully automatic property approach and the fully manual imperative approach.
When to Use Wire vs. Imperative
Use the wire service when you want data that stays in sync with changing inputs automatically — things like record detail pages where the record ID drives what data to fetch. Use imperative calls when you need to control timing explicitly — search buttons, save operations, form submissions, or any situation where the user triggers the call.
A common mistake is trying to use the wire service for everything. If you need to perform DML, you cannot use wire. If you need to call Apex only after some validation passes, imperative is cleaner. If you need to chain two Apex calls where the second depends on the first, imperative with async/await is the right tool.
How to Use the refreshApex Method
Here is a common scenario. You have a wired list of records displayed on the page. The user creates a new record through a form, and your imperative call saves it successfully. But the wired list does not update. It still shows the old data. This happens because the wire service caches responses. It does not know that you just changed something on the server.
The refreshApex function solves this. It tells the wire service to discard the cached data and re-fetch from the server.
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getContactsByAccount from '@salesforce/apex/ContactController.getContactsByAccount';
import createContact from '@salesforce/apex/ContactController.createContact';
export default class ContactManager extends LightningElement {
@api recordId;
firstName = '';
lastName = '';
wiredContactsResult;
@wire(getContactsByAccount, { accountId: '$recordId' })
wiredContacts(result) {
this.wiredContactsResult = result;
if (result.data) {
this.contacts = result.data;
this.error = undefined;
} else if (result.error) {
this.contacts = [];
this.error = result.error;
}
}
contacts = [];
error;
async handleSave() {
try {
await createContact({
firstName: this.firstName,
lastName: this.lastName,
accountId: this.recordId
});
this.firstName = '';
this.lastName = '';
await refreshApex(this.wiredContactsResult);
} catch (error) {
this.error = error;
}
}
}
The critical detail is what you pass to refreshApex. You do not pass the data array. You pass the entire result object that the wire service provided. That is why we store result in this.wiredContactsResult inside the wired function — we need to hold onto the full provisioned value, not just the data inside it.
If you wire to a property instead of a function, the pattern is slightly different. You pass the property itself.
@wire(getContactsByAccount, { accountId: '$recordId' })
contacts;
async handleSave() {
await createContact({ /* params */ });
await refreshApex(this.contacts);
}
Either way, refreshApex returns a Promise, so you can await it. Once it resolves, the wired property or function will have been called again with fresh data, and your template will update.
How to Create Salesforce Objects in JSON and Send Them to Apex via Your LWC
Sometimes you need to send a full SObject to Apex rather than individual field values. Maybe you are building a form that creates a record, or you are editing multiple fields and want to send the whole thing in one shot. You can construct an SObject-like JavaScript object and pass it to your Apex method.
The Apex Controller
Your Apex method can accept an SObject type directly as a parameter. The platform handles the deserialization from JSON for you.
public with sharing class ContactController {
@AuraEnabled
public static Contact createContact(Contact con) {
insert con;
return con;
}
@AuraEnabled
public static List<Contact> createContacts(List<Contact> contacts) {
insert contacts;
return contacts;
}
}
The LWC JavaScript
On the JavaScript side, you build a plain object with the SObject field API names as keys. You also need to include the sobjectType field if the Apex parameter is a generic SObject, but when the parameter type is a specific SObject like Contact, the platform infers the type automatically.
import { LightningElement, api } from 'lwc';
import createContact from '@salesforce/apex/ContactController.createContact';
import createContacts from '@salesforce/apex/ContactController.createContacts';
export default class ContactCreator extends LightningElement {
@api recordId;
firstName = '';
lastName = '';
email = '';
async handleCreateSingle() {
const newContact = {
FirstName: this.firstName,
LastName: this.lastName,
Email: this.email,
AccountId: this.recordId
};
try {
const result = await createContact({ con: newContact });
console.log('Created contact with Id:', result.Id);
} catch (error) {
console.error('Error creating contact:', error);
}
}
async handleCreateMultiple() {
const contactsToCreate = [
{
FirstName: 'Alice',
LastName: 'Johnson',
Email: 'alice@example.com',
AccountId: this.recordId
},
{
FirstName: 'Bob',
LastName: 'Smith',
Email: 'bob@example.com',
AccountId: this.recordId
}
];
try {
const results = await createContacts({ contacts: contactsToCreate });
console.log('Created contacts:', results.length);
} catch (error) {
console.error('Error creating contacts:', error);
}
}
}
Notice the field names use the Salesforce API names — FirstName, LastName, AccountId — not camelCase JavaScript names. This is important. The platform maps these keys directly to the SObject fields during deserialization on the server. If you use firstName instead of FirstName, the value will not make it to the Apex parameter.
You can also set relationship fields by providing the related record Id. For example, setting AccountId on a Contact links it to that Account. You do not need to pass a nested Account object.
One more pattern worth knowing: if your Apex method accepts a generic SObject parameter rather than a typed one, you need to include the sobjectType key in your JavaScript object.
const record = {
sobjectType: 'Contact',
FirstName: 'Test',
LastName: 'User'
};
This tells the platform which SObject type to deserialize the JSON into. In practice, you almost always type your Apex parameters explicitly, so this is rare, but it is good to know it exists.
JS Exception Handling for Apex Calls
Errors are going to happen. The network might fail. A validation rule might fire. A governor limit might get hit. Your LWC needs to handle all of these gracefully. The way errors surface depends on whether you are using imperative calls or the wire service, but the error object structure is the same.
Error Object Structure
When an Apex call fails, the error object you receive in your LWC has this shape:
{
body: {
message: "The error message from Apex or the platform",
stackTrace: "The Apex stack trace (if available)",
exceptionType: "System.DmlException"
},
ok: false,
status: 400,
statusText: "Bad Request"
}
The body.message field is what you typically display to the user. The body.exceptionType and body.stackTrace are useful for debugging but should generally not be shown in production UIs.
Handling Errors in Imperative Calls
With imperative calls, you use standard JavaScript try/catch or .then()/.catch() patterns.
import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import saveAccount from '@salesforce/apex/AccountController.saveAccount';
export default class AccountForm extends LightningElement {
async handleSave() {
try {
const result = await saveAccount({ /* params */ });
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Account saved successfully',
variant: 'success'
})
);
} catch (error) {
this.handleError(error);
}
}
handleError(error) {
let message = 'An unknown error occurred';
if (error.body) {
message = error.body.message;
} else if (error.message) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
}
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: message,
variant: 'error',
mode: 'sticky'
})
);
console.error('Full error details:', JSON.stringify(error));
}
}
The handleError method checks multiple possible shapes for the error. This defensive approach is important because errors can come from different layers — the Apex runtime, the network, or the LWC framework itself — and they do not always have the same structure.
Handling Errors in Wired Methods
With the wire service, errors arrive through the error property.
@wire(getContactsByAccount, { accountId: '$recordId' })
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
this.error = undefined;
} else if (error) {
this.contacts = [];
this.error = error;
console.error('Wire service error:', JSON.stringify(error));
}
}
You cannot use try/catch with the wire service because you are not calling anything — the framework is. The error comes to you as part of the provisioned result. Always handle both data and error cases in your wired function. If you only handle data, a failed call will silently leave your component in a stale state.
Throwing Custom Exceptions from Apex
On the Apex side, you can throw AuraHandledException to send a clean, user-friendly error message back to the LWC. This is the recommended approach for business logic errors.
public with sharing class AccountController {
@AuraEnabled
public static Account saveAccount(Account acc) {
if (String.isBlank(acc.Name)) {
throw new AuraHandledException('Account name is required.');
}
try {
upsert acc;
return acc;
} catch (DmlException e) {
throw new AuraHandledException(e.getMessage());
}
}
}
The AuraHandledException message goes straight into the error.body.message field on the client. If you throw a regular exception instead, the platform will send a generic error message to the client for security reasons — it does not want to leak internal details. So always catch your exceptions in Apex and re-throw them as AuraHandledException with a meaningful message if you want the LWC to display something useful.
A Reusable Error Utility
If you have multiple components calling Apex, it makes sense to extract the error handling into a shared utility.
// force-app/main/default/lwc/errorUtils/errorUtils.js
const reduceErrors = (errors) => {
if (!Array.isArray(errors)) {
errors = [errors];
}
return errors
.filter((error) => !!error)
.map((error) => {
if (Array.isArray(error.body)) {
return error.body.map((e) => e.message);
} else if (error.body && typeof error.body.message === 'string') {
return error.body.message;
} else if (typeof error.message === 'string') {
return error.message;
} else if (typeof error === 'string') {
return error;
}
return '';
})
.reduce((prev, curr) => prev.concat(curr), [])
.filter((message) => !!message);
};
export { reduceErrors };
Then in any component:
import { reduceErrors } from 'c/errorUtils';
// Inside your catch block or wired error handler:
const messages = reduceErrors(error);
console.error('Errors:', messages.join(', '));
This pattern comes straight from Salesforce’s own sample code and handles every error shape you will encounter — DML errors that return arrays, standard Apex errors, network errors, and plain strings. Building this once and importing it everywhere saves you from writing the same defensive code in every component.
Section Notes
This post covered the two main ways to get data from Apex into your Lightning Web Components — imperative calls and the wire service — along with refreshing cached data, sending SObject data to the server, and handling errors properly on both sides.
The key takeaways are:
- Imperative calls give you full control over timing. Use them for user-triggered actions, DML operations, and sequential logic. They return Promises and work naturally with
async/await. - The wire service is reactive and declarative. Use it when you want data to stay in sync with changing inputs automatically. Requires
cacheable=trueon the Apex method and cannot be used for DML. - refreshApex forces the wire service to re-fetch data after you know something has changed on the server. Always pass the entire provisioned value, not just the data.
- SObject parameters work by passing plain JavaScript objects with API field names as keys. The platform deserializes them into Apex SObject instances automatically.
- Error handling requires defensive code because errors can come from multiple layers with different shapes. Use
AuraHandledExceptionin Apex to send clean messages to the client, and build a reusablereduceErrorsutility for your components.
If you went through Part 83 on Lightning Data Service, you now have the full picture of data access in LWC. LDS handles simple single-record operations without any Apex code. Apex handles everything else — complex queries, multi-object operations, business logic, and callouts. Most real applications use both.
In the next section we will continue building on these patterns and look at more advanced LWC topics. See you there.