This post is also available in: Español (Spanish)
You cannot say that APEX exceptions are not self-explanatory for those who want to read them. What we can say indeed, is that the structure is just not user-friendly at all.
We can find situations in which we get errors like these:

Very clear errors for a Salesforce administrator or an APEX developer. But not explanatory at all for the standard user.
These are some of the situations in which we can run across errors in the user interface:
- Errors in a Process Builder (usually coming from APEX/server-side)
- Errors in a trigger before or a trigger validation before database modification
- Errors thrown by APEX and shown by a LWC
Note: if you feel like I’m missing other cases, please leave your comment about it so we can keep enriching this post. 🙂
Errors in Process Builder
Alright, not a good start, I know. But honestly, I’ve made a lot of research and just couldn’t find a way to force the Process Builder to show nice errors.
The only way I could achieve is by implementing a trigger before where I can indeed catch and control de error. Although this solution is only valid for before validations. (see next)
If anyone has achieved a cleaner solution for this issue, please leave a comment about it!
Errors thrown by a trigger before
Triggers are like “special classes” that are launched before or after a modification in the database. It can be an insertion(insert), a modification (update) or a deletion (delete).
APEX Triggers universe is quite diverse and exciting. But in this case, and focusing on the trigger before one, these are launched before a modification is made into the database. It’s recommendable to use them when you have discarded other native / UI functions For instance, the field validation rules.
When we implement a trigger, we need to specify what error message it has to show, with the method .addError(‘my error’).
It is important to remember that the addError() method has to be attached to the inserted object (or the object that triggered the process). Otherwise, it won’t work.
Please find below a trigger before example. I usually code my triggers using a “framework” structure that helps me keep my trigger logic-less and readable.
Trigger
trigger IdeasVoteTrigger on sonn_Idea_Vote__c (before insert) {
if (trigger.isBefore) {
if(trigger.isInsert) {
ideasVoteHandler.beforeInsertActions(Trigger.new);
}
}
}
Handler
public with sharing class IdeasVoteHandler {
public static void beforeInsertActions (List<sonn_Idea_Vote__c> newVotesForIdeas) {
IdeasHelper.checkIfUserHasVoted(Trigger.New);
}
}
Helper Functions
public with sharing class IdeasHelper {
public static List<sonn_Idea_Vote__c> checkIfUserHasVoted(List<sonn_Idea_Vote__c> newVotesForIdeas) {
if(newVotesForIdeas.isEmpty()){
return null;
}
Set<String> newVotesKeys = new Set<String>();
for (sonn_Idea_Vote__c vote : newVotesForIdeas ){
//We need to create the keySet in order to filter the SOQL
String userNoCapsInsentive = String.valueOf(UserInfo.getUserId()) ;
userNoCapsInsentive = userNoCapsInsentive.Substring(0, (userNoCapsInsentive.length()-3));
String ideaNoCapsInsensitive = String.valueOf(vote.sonn_Idea__c);
ideaNoCapsInsensitive = ideaNoCapsInsensitive.Substring(0, (ideaNoCapsInsensitive.length()-3));
String newVoteKey = userNoCapsInsentive +'-' + ideaNoCapsInsensitive ;
newVotesKeys.add(newVoteKey);
}
List<sonn_Idea_Vote__c> alreadyVotedIdeas = [SELECT ID, sonn_VoteKey__c, CreatedById, sonn_Idea__c FROM sonn_Idea_Vote__c WHERE sonn_VoteKey__c IN :newVotesKeys];
for (sonn_Idea_Vote__c oldVote : alreadyVotedIdeas) {
for(sonn_Idea_Vote__c newVote: newVotesForIdeas) {
String userNoCapsInsentive = String.valueOf(UserInfo.getUserId()) ;
userNoCapsInsentive = userNoCapsInsentive.Substring(0, (userNoCapsInsentive.length()-3));
String ideaNoCapsInsensitive = String.valueOf(newVote.sonn_Idea__c);
ideaNoCapsInsensitive = ideaNoCapsInsensitive.Substring(0, (ideaNoCapsInsensitive.length()-3));
String newVoteKey = userNoCapsInsentive +'-' + ideaNoCapsInsensitive ;
if (oldVote.sonn_VoteKey__c == newVoteKey){
newVote.addError('The user has already voted for this idea. You cannot vote twice');
}
}
}
return newVotesForIdeas;
}
}
In case we use our trigger with a general-purpose within our Salesforce org by simply adding the method .addError() will show a nice user-friendly error in the user interface. This is how we can achieve a nice error for a Process Builder (yeah, it’s kind of cheating).
Errors thrown by APEX and shown by a LWC
Let’s keep with our trigger example. Let’s put a button that inserts a vote from a LWC. If the vote already exists for that user, then APEX will return the error message of the .addError() exception.
To make the exception travel from APEX to LWC we need to create a new AuraHandledException(). Otherwise, the error will be shown only serverside and will never reach the front.
@AuraEnabled
public static void createVote(String IdeaId){
try {
sonn_Idea_Vote__c newVote = new sonn_Idea_Vote__c();
newVote.sonn_Idea__c = IdeaId;
insert newVote;
} catch (Exception e) {
String errorMsg = e.getMessage();
throw new AuraHandledException(ErrorMsg);
}
}
}
Now, let’s make our front-end collect the error and display it. There are several ways to achieve this. But I personally like to use the toast component.
LWC
import {
LightningElement,
api,
} from 'lwc';
import {
ShowToastEvent
} from 'lightning/platformShowToastEvent'
import createVote from '@salesforce/apex/IdeasHelper.createVote'
export default class IdeaActions extends LightningElement {
@api recordId;
@api ideaTitle;
@api votes;
showToast(title, message, variant) {
const event = new ShowToastEvent({
title: title,
message: message,
variant: variant
});
this.dispatchEvent(event);
}
async createIdea(e) {
try {
await createVote({
IdeaId: this.recordId
})
this.showToast('Register Vote', 'Your vote was succesfully registered', 'success');
this.votes++
} catch (error) {
this.showToast('Something went wrong', error.body.message, 'error');
return;
}
}
}
When the user clicks the “support” button, this will call the createIdea(e) method. This is the function that invokes the APEX class that wants to insert the vote into de database. And triggers the validation to check if the vote existed or not. And this is what the LWC method returns:

Just as we defined it, the APEX method is returning the full exception body. In order to get a nice clean error message in LWC, we just need to send it a nice clean text first! If we know whats the structure of the exception, then something like this will lead us the expected result:
@AuraEnabled
public static void createVote(String IdeaId){
try {
sonn_Idea_Vote__c newVote = new sonn_Idea_Vote__c();
newVote.sonn_Idea__c = IdeaId;
insert newVote;
} catch (Exception e) {
String errorMsg = e.getMessage();
String pureErrorMsg = errorMsg.substringAfter('_EXCEPTION,');
pureErrorMsg = pureErrorMsg.Substring(0, (pureErrorMsg.length()-4));
throw new AuraHandledException(ErrorMsg);
}
}
}
And once we treat the string with some fancy String methods to keep only what we want from the text, we get a LWC error like this:

Errors shown in LWC coming from validation rules
Another easy way of controlling the display error messages is by using thenative validation rules of Salesforce. This comes in handy to implement simple validations of some field’s content within the same object or other related objects. But when we need complex validation such as, for instance, check if there was already a vote created, we are forced to create a trigger before.
In this same example, we want that when the button is clicked it also checks that the voter is not the creator of the idea. This can be easily achieved with a validation rule from Object Manager > Object > Validation Rules:

Now we define in the formula when it has to throw the error: when the active user ($User.Id) is equal to creator of the idea:

And finally let’s define the error.

And that’s it! Almost. If we were just validating a field the user is filling, then the error would be display in red below that field or on the top of the app. But here we want to catch it and display it in a Toast from our LWC.
Actually, if we have already implemented the same code I’ve shown before, we really do not need to do anything else. The LWC will receive de validation rule exception cleaned by the APEX controller and show it clean in the toast.
I really hope this post helps you find ideas to control the errors and give your users a better error experience. As I have mentioned above, please leave in comments any new solutions or ideas I haven’t covered or thought of.
Peace and Code