---
title: "Error handling in imperative Apex calls"
url: "https://trailassembly.com/lwc-apex-imperative-error-handling/"
topic: "LWC and Front-End"
published: "2026-09-18T08:43:35+00:00"
updated: "2026-10-06T13:57:58+00:00"
error_codes: "AuraHandledException, DmlException, NullPointerException"
---

# Error handling in imperative Apex calls

**Short answer:** an uncaught Apex exception reaches the browser as a generic internal server error with none of your message in it. To send the user something useful you must throw an `AuraHandledException` carrying a message you wrote, catch the rejection in JavaScript, pull the text out of `error.body.message`, and render it where the user is looking. Every step is easy to get wrong, and the usual result is a component that says “An internal server error has occurred” for six different underlying problems.

## What actually crosses the wire

An imperative call returns a promise. The rejection value is not an Apex exception. It is a plain object the platform assembled from one.

```apex
{
    status: 500,
    body: { message: 'Credit limit exceeded for this account.',
            exceptionType: 'System.AuraHandledException',
            isUserDefinedException: true },
    headers: {},
    ok: false
}
```

Throw a `DmlException`, a `NullPointerException`, or let a null reference escape, and the client gets a message telling it an internal server error occurred, plus a reference id. Your text is not in there. That is deliberate, because uncaught server exceptions can carry information about internals that should not be shown, so the platform replaces the message.

`AuraHandledException` is the exception type the platform treats as safe, and it is the only one whose message is delivered as written.

## Throwing properly

```apex
@AuraEnabled
public static void submitOrder(Id orderId) {
    try {
        Order__c order = [SELECT Id, Status__c, Total__c FROM Order__c
                          WHERE Id = :orderId WITH USER_MODE];
        if (order.Total__c > getCreditLimit(order)) {
            throw new OrderException('Order total exceeds the account credit limit.');
        }
        order.Status__c = 'Submitted';
        update as user order;
    } catch (OrderException e) {
        throw new AuraHandledException(e.getMessage());
    } catch (Exception e) {
        Logger.log(e);
        throw new AuraHandledException(
            'The order could not be submitted. Reference: ' + e.getMessage());
    }
}
```

Two rules here. Wrap your business rules in an exception type of your own so you can tell them apart from genuine failures, and never let a raw system exception through without logging it first. Once you convert to `AuraHandledException`, the stack trace is gone from the client, so if you did not log it server-side it is gone entirely.

Think about what a technical failure should say. The raw text of a `DmlException` hands the user a field name they have never heard of, though the [validation rule](https://trailassembly.com/validation-rule-vs-flow/) message buried inside it is sometimes precisely what they need.

A quirk worth knowing in tests: constructing `new AuraHandledException('text')` and then calling `getMessage()` in Apex may return “Script-thrown exception” rather than your text. Calling `setMessage()` on the instance avoids the surprise if your test asserts on the message. The client still receives the text you supplied.

## Catching in JavaScript

```apex
import submitOrder from '@salesforce/apex/OrderController.submitOrder';

async handleSubmit() {
    this.isSaving = true;
    this.errors = [];
    try {
        await submitOrder({ orderId: this.recordId });
        this.dispatchEvent(new ShowToastEvent({
            title: 'Order submitted', variant: 'success' }));
    } catch (error) {
        console.error(error);
        this.errors = reduceErrors(error);
    } finally {
        this.isSaving = false;
    }
}
```

The `finally` block matters more than it looks. A spinner that is only cleared in the success path stays on screen forever when the call fails, and users read that as a hung page.

Reading `error.body.message` directly works for a single `AuraHandledException` and breaks for everything else. A page-level error from the User Interface API gives you `body` as an array. A network failure gives you no `body` at all, only `statusText`. Use a `reduceErrors` helper that checks all three shapes, and keep one copy of it in a shared utility module.

Parameter names are the other silent failure. The object keys passed from JavaScript must match the Apex parameter names exactly, including case. A mismatch produces a rejection that reads like a server problem and is actually a typo.

## Messages that are worth reading

- Name the thing. “Order 00123 could not be submitted” beats “Submission failed”.

- Say whether retrying helps. A credit limit will not fix itself. A timeout might.

- Give a reference for anything unexpected, and log the same reference server-side.

- Do not include the class name, the line number, or the word “exception”.

And keep the message on screen. An error that appears in a toast and vanishes after a few seconds has told the user nothing they can act on. Render it inline, near the control that triggered the call, with `role="alert"` so it is announced.

## Frequently asked

**Why is my custom message missing?** The exception was not an `AuraHandledException`, so the platform replaced it with a generic one.

**Should I return an error object instead of throwing?** No. A thrown exception rolls back the transaction. A returned status code leaves partial writes committed.

**How do I see the real stack trace?** Log it in Apex before converting, or reproduce with a debug log open. It never reaches the browser.

---

Source: https://trailassembly.com/lwc-apex-imperative-error-handling/
Trail Assembly — independent Salesforce write-ups.
