Skip to main content

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:

SourceError pathExample
AuraHandledExceptionerror.body.message"Only draft orders can be submitted."
Unhandled Apex exceptionerror.body.message"An Apex error occurred: ..."
Wire service errorerror.body.messageSame as above
Network/connectivityerror.message"Failed to fetch"
JavaScript errorerror.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

ScenarioApproach
Validation failure (business rule)throw new AuraHandledException('Clear message')
DML failureCatch, log, re-throw as AuraHandledException
Callout failureCatch, log, re-throw with sanitized message
Unexpected errorCatch-all, log full stack trace, throw generic user message
Client-side validationHandle entirely in JavaScript before calling Apex

Key Takeaways

  • Always wrap system exceptions in AuraHandledException before 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.