Errores amigables en Salesforce LWC + APEX

Este artículo también está disponible en: English (Inglés)

No podemos decir que para el que presta atención los errores de APEX no sean claros. Pero sí podemos afirmar que si no se tratan no son nada amigables para el usuario.

Podemos encontrarnos con varias situaciones en las que obtenemos errores de este tipo:

Errores bastante explicativos para un administrador o un programador APEX, pero poco amigables para el usuario.

Estos son algunos de los casos en los que podemos encontrar errores así en la interfaz:

  1. Errores en un Process Builder (normalmente provienen de APEX)
  2. Errores de trigger before o validación por trigger
  3. Errores de APEX mostrados por un LWC

Nota: si se escapa alguno, dejarlo en comentarios y vamos enriqueciendo el post 🙂

Errores en Process Builder

Bueno, ya empiezo mal, pero por más que he buscado formas de capturar errores y forzar a que un Process Builder lo muestre bonito, no la he encontrado.

La única forma que he tenido de esto es pasar parte de la lógica a un trigger de APEX donde sí puedo controlar el error que devuelve. (ver siguiente)

Si alguien ha dado con la fórmula para controlar los errores que devuelve un PB, por favor decídmelo.

Errores en un trigger before

Los triggers o disparadores son «clases especiales» que se lanzan antes o después de una modificación en la base de datos. Ya sea inserción (create), modificación (update) o eliminación (delete).

El mundo de los triggers en APEX es bastante diverso y emocionante. En este caso, los trigger before, son disparadores que se lanzan antes de que se haga la modificación. Es recomendable utilizarlos cuando otras opciones nativas / UI no se pueden usar. Como por ejemplo, las validaciones de campo (validation rules).

Cuando implementamos un trigger before, nos permite especificar qué error debe lanzar, con el método .addError(‘my error’).

Es importante que el método addError() se adjunte al objeto que se está insertando, de lo contrario, no funcionará.

A continuación os pongo un ejemplo de uso de trigger before. Yo normalmente utilizo un «framework» en el que llamo clases auxiliares para mantener el código del trigger lo más vacío de lógica posible.

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;

    }
}

Si implementamos nuestro trigger para ser ejecutado en general antes de una inserción, normalmente agregarle el método .addError() hará que en la interfaz de Salesforce se muestre bonito. Por ejemplo, si queremos validar información antes de que se lance un Process Builder, es la forma de hacerlo y lo mostrará bien sin que el Process Builder falle con un error extraño.

Errores de APEX mostrados por un LWC

Continuemos con el trigger. Vamos a poner un botón que intente insertar el voto desde un LWC. Si el voto está duplicado, entonces le devolverá el mensaje de .addError() como una excepción.

Y para que viaje desde APEX hasta LWC, tenemos que crear una AuraHandledException(). De lo contrario el error sólo se mostrará del lado del servidor y nunca se mostrará en LWC.

 @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);
        }

    }

}

Ahora, tenemos que hacer que el error se muestre en nuestro front-end. Podemos hacerlo de varias maneras, pero personalmente me gusta mucha utilizar el componente toast.

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;
        }
    }

}

Así cuando pinchen en el botón «support» éste llamará a la función createIdea(e). Esta función es la que invoca el APEX que quiere insertar el voto en la base de datos. Y por tanto la que dispara el trigger que comprueba si este voto no existe previamente. Y esto es lo que devuelve:

Oups, nada friendly-exception

Como le dijimos, le está devolviendo al LWC la excepción completa. Para obtener el error limpio, simplemente tenemos que pasarle el texto «bonito» al LWC. Si sabemos cuál es la estructura de nuestra excepción entonces algo así nos llevará al resultado:

 @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);
        }

    }

}

Y una vez tratamos la cadena con métodos String para quedarnos con lo único que nos interesa, obtenemos un error en LWC así:

Errores en LWC con Validaciones de Campo

Otra forma muy sencilla de controlar el error mostrado es utilizando las validaciones de campo nativas de Salesforce. Nos vale para implementar soluciones sencillas como determinados contenidos en los campos del mismo objeto o de otros objetos relacionados. Pero cuando necesitamos validaciones complejas de datos como, por ejemplo, comprobar que no hubiese ya un voto creado, nos vemos forzados a utilizar un trigger before.

En este mismo ejemplo, queremos que cuando le den al mismo botón, compruebe que quien quiere votar no es el creador de la idea. Esto sí lo podemos obtener fácilmente como una validación de campo desde el Object Mananager > Object > Validation Rules:

Ahora le decimos en la fórmula que compruebe que el usuario activo ($User.Id) no es igual que el creador de la idea:

Y finalmente le indicamos qué error queremos que muestre:

¡Y ya está! Bueno, casi. Si lo que estuviésemos validando fuese el contenido de un campo que introduce el usuario, este error se mostraría como un mesaje en rojo debajo de ese campo. Pero aquí queremos capturarlo y mostrarlo en una Toast.

Realmente si hemos hecho lo anterior, no necesitamos hacer nada más. El LWC que hemos escrito antes recibirá este error de la validación, lo limpiará y se lo pasará limpio a la toast.

Espero que este post os haya ayudado a coger ideas para controlar los errores y devolverle pistas más amigables a vuestros usuarios. Como ya mencioné, dejad en comentarios si tenéis nuevas soluciones o más breves, que seguro no ayudan a todos.

Peace and Code

Añade tu respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *