SfSkills Domains Docs GitHub

Home / lwc / lwc-imperative-apex

lwc-imperative-apex

Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for reactive @wire reads — use lwc/wire-service-patterns. NOT for mocking the call in a Jest test — use lwc/lwc-testing.

Open the skill folder on GitHub


LWC Imperative Apex

This skill activates when a Lightning Web Component needs to call an Apex method on demand — in response to a button click, a lifecycle hook, or explicit conditional logic — rather than reactively through the wire service. It covers the full pattern from import syntax through error handling and parallel calls.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Mode 1 — Import and Call With Promise Chain

The fundamental imperative pattern imports the Apex method as a module and calls it as a function that returns a Promise.

import getContacts from '@salesforce/apex/ContactController.getContacts';

// In a handler or lifecycle hook:
getContacts({ accountId: this.recordId })
    .then(result => {
        this.contacts = result;
        this.error = undefined;
    })
    .catch(error => {
        this.error = error;
        this.contacts = undefined;
    });

The import path always follows @salesforce/apex/<ClassName>.<methodName>. The Apex method must be static and annotated @AuraEnabled. Parameters are passed as a plain object where keys match the Apex parameter names exactly (case-sensitive).

Mode 2 — Async/Await With Loading State

The async/await form is more readable when the handler has sequential operations or multiple calls. Wrap it in a try/catch block and manage an isLoading boolean for UI feedback.

async handleSave() {
    this.isLoading = true;
    try {
        const result = await saveRecord({ recordData: this.draftValues });
        this.dispatchEvent(new ShowToastEvent({ title: 'Saved', variant: 'success', message: result }));
        this.draftValues = [];
    } catch (error) {
        this.error = error;
    } finally {
        this.isLoading = false;
    }
}

The finally block guarantees isLoading is cleared even if the Apex throws. This avoids a spinner that never stops — one of the most common UX bugs in LWC Apex integration.

Mode 3 — Parallel Calls With Promise.all

When the component needs two or more independent Apex results simultaneously (for example, loading related data sets for a detail view), run them in parallel rather than sequentially:

async connectedCallback() {
    this.isLoading = true;
    try {
        const [accounts, contacts] = await Promise.all([
            getAccounts(),
            getContacts({ accountId: this.recordId })
        ]);
        this.accounts = accounts;
        this.contacts = contacts;
    } catch (error) {
        this.error = error;
    } finally {
        this.isLoading = false;
    }
}

Sequential await calls add latency because each waits for the prior round-trip. Promise.all fires both calls concurrently and resolves when both complete. If any call rejects, the catch fires and the entire batch is treated as failed — design accordingly.


Common Patterns

Cacheable Read — Data Displayed on Load

When to use: The component loads record data, picklist values, or lookup results that do not change until the user explicitly refreshes the page.

How it works:

  1. Annotate the Apex method @AuraEnabled(cacheable=true).
  2. Import and call in connectedCallback (or a handler triggered by input changes).
  3. Store the result in a reactive property so the template re-renders automatically.
import getAccountSummary from '@salesforce/apex/AccountController.getAccountSummary';

async connectedCallback() {
    try {
        this.summary = await getAccountSummary({ accountId: this.recordId });
    } catch (error) {
        this.error = getErrorMessage(error);
    }
}

Why not wire: Wire is appropriate when the data should reactively refresh whenever reactive properties change. For one-shot load on mount, imperative is simpler and avoids the wire provisioning lifecycle.

Non-Cacheable Write — DML on User Action

When to use: A button or form submission should create, update, or delete records. Cacheable methods cannot perform DML; attempting it throws a runtime error.

How it works:

  1. Annotate the Apex method @AuraEnabled (no cacheable=true).
  2. Call it in the click handler.
  3. After a successful write, re-call any read methods that need fresh data — refreshApex() does not apply here.
import upsertContact from '@salesforce/apex/ContactController.upsertContact';

async handleSubmit() {
    this.isLoading = true;
    try {
        await upsertContact({ contact: this.draftContact });
        // Re-fetch to get the latest state
        this.contacts = await getContacts({ accountId: this.recordId });
    } catch (error) {
        this.error = getErrorMessage(error);
    } finally {
        this.isLoading = false;
    }
}

Error Message Extraction

The error object from a rejected Apex call is an object with a body property. Extract a readable string before displaying it:

function getErrorMessage(error) {
    if (Array.isArray(error.body)) {
        return error.body.map(e => e.message).join(', ');
    }
    if (typeof error.body?.message === 'string') {
        return error.body.message;
    }
    return error.message ?? 'Unknown error';
}

Using error.message directly without checking body fails silently — the user sees "undefined" in the UI.


Decision Guidance

SituationRecommended ApproachReason
Data loads once on component mountImperative call in connectedCallbackSimpler than wire for non-reactive scenarios
Data must refresh when a property changes@wire adapterWire re-runs automatically on reactive property change; imperative requires manual re-call
Button triggers a DML operationNon-cacheable imperative callCacheable methods cannot perform DML
Two independent data sets needed on loadPromise.all with imperative callsParallel calls are faster than sequential awaits
Need to refresh after a writeRe-call the read method imperativelyrefreshApex() only works with wire results
Reading standard objects with field-level securityWire LDS adapters (getRecord)LDS handles FLS automatically; custom Apex must enforce it explicitly

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. refreshApex() silently does nothing on imperative results — refreshApex() is a wire-only utility. Calling it on a variable populated by an imperative call neither throws nor refreshes; data simply stays stale. Re-call the method explicitly to get fresh data.
  2. cacheable=true blocks all DML, including Database.insert inside helper methods — If a cacheable=true method calls any helper that performs DML (even via a private method chain), Salesforce throws a runtime error: System.QueryException: Callout not allowed after DML. Mark the method non-cacheable if any part of the call stack writes to the database.
  3. Parameter names are case-sensitive and must match Apex exactly — Passing { AccountId: id } when the Apex parameter is accountId results in a null value in Apex with no error thrown. Debug by adding a null check in Apex or verifying the parameter name case.

Output Artifacts

ArtifactDescription
Imperative Apex call patternJS snippet with import, async handler, isLoading, and error extraction ready to paste into a component
Error extraction utilityReusable getErrorMessage(error) function that handles both single-message and array-form body shapes
Parallel load patternPromise.all snippet for loading multiple independent data sets in connectedCallback

Package files