Admins: works great.
Buyers: "No locations are assigned to your account."
There is no worse bug than one that shows up as a polite, reasonable-sounding message.
The component had been rebuilt with a brand new, well-tested Apex controller. The data was fine. The problem was that the new class wasn't in the site's permission sets. And the component's @wire error handler did console.error and nothing else, so "you're not allowed to call this" rendered as "you have nothing."
Two lessons, in order of how much they cost me.
1. Every Apex class an Experience Cloud user calls needs an explicit grant
Don't guess how access is set up. Find how the existing classes are granted and mirror it exactly:
SELECT SetupEntityId, Parent.Name, Parent.Type, Parent.IsOwnedByProfile, Parent.Profile.Name
FROM SetupEntityAccess
WHERE SetupEntityId IN (SELECT Id FROM ApexClass WHERE Name IN ('OldController', 'NewController'))
In this org the old class sat on one permission set and one permission set group. The new one was on the System Administrator profile. That's it. Which is exactly why it worked for me.
And before adding a class at all, ask whether a small extension of an already-granted one is the smaller change. Here it was. The redesign ended up calling the same two methods the old component called, reshaping the response on the client. The only Apex change was seven extra fields in an existing query. Less new code, zero new access to get wrong.
2. Never let a failed call look like empty data
Keep the error. Put it on screen.
// Apex errors arrive as { body: { message } }; wire errors sometimes as an
// array of those; anything else falls back to its own message.
function errorMessage(error) {
const first = Array.isArray(error) ? error[0] : error;
return first?.body?.message ?? first?.message ??
(typeof first === "string" ? first : "Unknown error");
}
@wire(getLocationAccounts, { contactId: "$contactId" })
wiredLocations({ data, error }) {
if (data) {
this.locations = data;
this.locationsError = undefined;
} else if (error) {
this.locationsError = errorMessage(error);
}
}
get noLocationsMessage() {
return this.locationsError
? `Locations couldn't be loaded. ${this.locationsError}`
: "No locations are assigned to your account.";
}
An ugly error message gets a ticket filed in five minutes. A believable empty state gets a week of "is the data wrong?"
Bonus round
When I went to delete the abandoned class, Salesforce refused: "This apex class is referenced elsewhere in Salesforce … Web App Resource Dependency."
An LWR site's published bundle remembers which Apex its components imported, even after the component stops importing it. Republish the site, then delete.