Error Handling Patterns: From Apex Controllers to Lightning Web Components
Error handling across the Apex-to-LWC boundary is surprisingly inconsistent. Errors arrive in different shapes depending on their origin, and without a deliberate strategy, user-facing messages end up as cryptic stack traces or silent failures. This article covers the patterns that produce reliable, user-friendly error handling.
Two Types of Apex Errors
AuraHandledException (User-Facing)
When you know an error should be shown to the user, throw an AuraHandledException. The message you set is what the LWC receives -- no stack trace leakage.
@AuraEnabled
public static void submitOrder(Id orderId) {
Order__c ord = [SELECT Id, Status__c FROM Order__c WHERE Id = :orderId];
if (ord.Status__c != 'Draft') {
throw new AuraHandledException(
'Only draft orders can be submitted. Current status: ' + ord.Status__c
);
}
// proceed with submission...
}
Standard Exceptions (System Errors)
Unhandled exceptions (NullPointerException, DmlException, QueryException) propagate to the LWC as system errors. Their messages are often technical and unhelpful to end users.
@AuraEnabled
public static void processRecords(List<Id> recordIds) {
try {
// business logic that might fail
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
update accounts;
} catch (DmlException e) {
// Wrap system error in a user-friendly message
throw new AuraHandledException(
'Unable to process records: ' + e.getDmlMessage(0)
);
} catch (Exception e) {
// Log the full error for debugging
System.debug(LoggingLevel.ERROR, 'processRecords failed: ' + e.getMessage());
throw new AuraHandledException(
'An unexpected error occurred. Please contact your administrator.'
);
}
}
The key principle: catch system exceptions, log them for developers, and re-throw as AuraHandledException with a clean message for users.
How Errors Arrive in LWC
The error object shape varies depending on the source:
| Source | Error path | Example |
|---|---|---|
| AuraHandledException | error.body.message | "Only draft orders can be submitted." |
| Unhandled Apex exception | error.body.message | "An Apex error occurred: ..." |
| Wire service error | error.body.message | Same as above |
| Network/connectivity | error.message | "Failed to fetch" |
| JavaScript error | error.message | "Cannot read property 'Id' of undefined" |
A Universal Error Extraction Utility
Because errors arrive in multiple shapes, a utility function normalizes them:
/**
* Extracts a human-readable error message from any error shape
* returned by Apex, wire services, or JavaScript.
*/
export function extractErrorMessage(error) {
// AuraHandledException or wire service error
if (error?.body?.message) {
return error.body.message;
}
// Array of field-level errors
if (error?.body?.fieldErrors) {
return Object.values(error.body.fieldErrors)
.flat()
.map(e => e.message)
.join('; ');
}
// Page-level errors array
if (Array.isArray(error?.body?.pageErrors)) {
return error.body.pageErrors.map(e => e.message).join('; ');
}
// Standard JavaScript error
if (error?.message) {
return error.message;
}
// String error
if (typeof error === 'string') {
return error;
}
return 'An unknown error occurred.';
}
Place this in a shared utility module (e.g., c/utils) and import it wherever errors are handled.
LWC Error Handling in Practice
import { LightningElement, api } from 'lwc';
import submitOrder from '@salesforce/apex/OrderController.submitOrder';
import { extractErrorMessage } from 'c/utils';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class OrderSubmit extends LightningElement {
@api recordId;
isProcessing = false;
async handleSubmit() {
this.isProcessing = true;
try {
await submitOrder({ orderId: this.recordId });
this.dispatchEvent(new ShowToastEvent({
title: 'Success',
message: 'Order submitted successfully.',
variant: 'success'
}));
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Error',
message: extractErrorMessage(error),
variant: 'error',
mode: 'sticky'
}));
} finally {
this.isProcessing = false;
}
}
}
When to Use Each Pattern
| Scenario | Approach |
|---|---|
| Validation failure (business rule) | throw new AuraHandledException('Clear message') |
| DML failure | Catch, log, re-throw as AuraHandledException |
| Callout failure | Catch, log, re-throw with sanitized message |
| Unexpected error | Catch-all, log full stack trace, throw generic user message |
| Client-side validation | Handle entirely in JavaScript before calling Apex |
Key Takeaways
- Always wrap system exceptions in
AuraHandledExceptionbefore they reach the LWC. - Never expose raw stack traces or internal field names to end users.
- Use a shared
extractErrorMessage()utility to normalize the inconsistent error shapes. - Log detailed error information server-side for debugging while keeping user messages clean.
- Use
mode: 'sticky'on error toasts so users can read the full message before it disappears.