Lightning Exception Handling Framework
There are 3 types of code: Bad code, functional code, and good code. Bad code simply doesn’t work and functional code works but has a bad user experience, but what makes good code “good”? There are a million ways to correctly answer that question, but I’m not writing on a million topics so I will point out a select few. Good code handles exceptions with grace and doesn’t repeat itself over and over (DRY method). In my last blog I talked about the Lightning Lookup input field. For this blog, I will be covering how to handle Lightning exceptions with code reusability.
Backstory
Over the past year of developing more and more on the LEX framework, I have noticed that I never found a good exception handling framework that I could use for my custom apps and components. I found myself writing the same exception handling code for each component individually and they all varied to a small degree. Now, I do not claim to be an expert by any means but I wanted to share my exception handling framework I have designed with anyone who is currently on the same journey as I was.
The Framework
The framework consists of 2 different concepts: Handling exceptions in Lightning and notifying developers or teams when certain exceptions are raised with detailed information on what happened. Having the right information, as you may know, speeds up the debugging process so application downtime is shortened.
The Component
This framework uses an extensible component that any custom component can extend. This component contains all of the server callback response handing and exception handling as well as a few exposed helper methods. Let’s take a look at the component.
<aura:component extensible="true" access="GLOBAL" controller="Log"> <!-- global attributes --> <aura:attribute name="devicetype" type="String" access="GLOBAL"/> <aura:attribute name="deviceinfo" type="DeviceInformation" access="GLOBAL"/> <aura:attribute name="emailOnError" type="Boolean" default="TRUE" access="GLOBAL"/> <!-- init handler --> <aura:handler name="init" value="{!this}" action="{!c.init}"/> <!-- Global methods --> <aura:method name="successfulResponse" access="GLOBAL"> <aura:attribute name="res" type="Object" required="true"/> </aura:method> <aura:method name="success" access="GLOBAL"> <aura:attribute name="msg" type="String" required="true" access="GLOBAL"/> <aura:attribute name="mode" type="String" required="false" access="GLOBAL"/> </aura:method> <aura:method name="error" access="GLOBAL"> <aura:attribute name="msg" type="String" required="true" access="GLOBAL"/> <aura:attribute name="mode" type="String" required="false" access="GLOBAL"/> </aura:method> <!-- Extending component body --> <div>{!v.body}</div> </aura:component>
This component has a few attributes and methods that any other component that extends this can use.
- Attribute: deviceinfo
- Description: DeviceInformation wrapper instance with user device information (Browser, OS, Desktop/Mobile, etc)
- Attribute: emailOnError
- Description: Determines if a notification email is sent if this component (only this component) fails to parse device information. Defaults to TRUE
- Method: successfulResponse(res)
- Description: parses the response from a server callback. “res” is the response object.
- Method: success(msg,[mode])
- Description: Fires a “Success” toast with the given message
- Method error(msg,[mode])
- Description: Fires a “Error” toast with the given message
The Log
I had a client last year that wanted a comprehensive logging application that would notify different departments of Apex errors based on what application the error came from. What we came up with was extremely useful for proactively dealing with and fixing application exceptions and minimizing the amount of downtime for the business and the customers. This framework implements a simplified, yet just as effective, version of that solution. Here are the details of the Log class:
public class Log { public static void log(String msg) // stores debug statement and prints to the debug log public static void log(String msg, Object o) // same as above. The Object, o, is serialized as part of the debug statement public static void notify(Object obj, String appName, DeviceInformation lla) // gathers exception data and sends an email public static void notify(String appName, String emailbody, String subject) // just send an already constructed email }
The “log” method is used for printing to the debug log (like using System.debug()) but it also stores that log entry for the email notification if an exception occurs. Adding the right debug statements in your code can help you know exactly why the exception occurred without further investigation.
In the method, notify(Object obj, String appName, DeviceInformation lla), the first variable in the method signature is a general object. It explicitly parses Exception, List<Database.SaveResult>, and List<Database.UpsertResult> objects, but it will also work with any type that is serializable (unlike SelectOption class, which cannot be serialized). The appName variable is used to query a custom metadata type called, “Log Notification Settings.” This metadata type uses MasterLabel as the appName and there is a field, “Email On Error”, that takes a comma-separated list of emails to email the exception details to. In the following example, I am using “General” as the appName.
The “notify” method should be used inside a catch block to gather the exception information and send the email.
Implementation
Now that we are somewhat familiar with what the design is using, let’s take a look at how to implement with a custom component.
<aura:component implements="flexipage:availableForAllPageTypes" extends="c:LightningLog" controller="ExampleLightningLogCmpController"> <aura:set attribute="notifyOnError" value="TRUE"/> <div>Device: {!v.devicetype}</div> <div> <lightning:button variant="brand" label="Click Me!" onclick="{!c.doSomething}"/> </div> </aura:component>
To extend the logging component to a new custom component, all that is needed is to add extends=”c:LightningLog” to the aura:component markup. Once that is in place, we now have access to the logging functionality in this new component.
As you can see in line 3-5, we have access to the log component’s attributes. If you need to override an attribute, like I am on line 3, you must use aura:set.
Let’s take a look at what the “Click Me!” button will do.
doSomething : function(component) { var action = component.get('c.doIt'); action.setParams({ 'lla' : JSON.stringify(component.get('v.deviceinfo')) }) action.setCallback(this,function(result){ if(!component.successfulResponse(result)){ console.log('error'); return; } component.set('v.deviceinfo',result.getReturnValue()); console.log(result.getReturnValue()); }); $A.enqueueAction(action); }
In this method, we are calling a server-side controller method and setting the result to the log component’s “deviceinfo” attribute. For the action parameters, I am passing in the DeviceInformation as a JSON string. I am doing this because I would like the device information to be in the exception email. Since the DeviceInformation is an Apex wrapper, the object needs to be serialized in order to be sent to the server. DeviceInformation has a static method to help you deserialize once inside the Apex method.
DeviceInformation llaObj = DeviceInformation.deserialize(lla);
Here’s the super cool part: Notice on line 8 that we are handling the server response in 1 line by calling the component.successfulResponse(res) method! The successfulResponse method returns a boolean (true for successful and false for errors). Having this response parsing and exception handling centralized helps keep the code clean, maintainable, and scalable.
Finally, here’s the Apex controller method that uses the framework:
@AuraEnabled public static DeviceInformation doIt(String lla){ Log.log('llan',lla); DeviceInformation ret = DeviceInformation.deserialize(lla); Savepoint sp = Database.setSavePoint(); try{ Log.log('query for account'); Account a = [Select Id,Industry from Account LIMIT 1]; Log.log('accountn',a); a.Name = 'Test 1234'; update a; } catch(Exception e){ Database.rollback(sp); ret.auraerror = e.getMessage(); Log.notify(e, 'General', ret); } return ret; }
Please note that if you throw an AuraHandledException object inside the catch block, the email queue will be rollbacked and no notification will be sent.
The important piece of this is the DeviceInformation variable, “auraerror”. Setting the error message to this variable and not raising an exception will allow for the user to see the error message and the exception email to be sent. This doesn’t only work for DeviceInformation class. Any class that has an @AuraEnabled variable named “auraerror” can be returned from the server and parsed in the log component. For example:
public class CanParse { @AuraEnabled String auraerror; @AuraEnabled Boolean anotherproperty; } public class CanNotParse { @AuraEnabled String returnString; }
The class, CanParse, is able to be read with the LightningLog component because it has the variable “auraerror” that is visible to the lightning component. CanNotParse does not have that variable so the LightningLog component will simply ignore it.
Lightning and Beyond
The Log apex class is written so that it can be used outside of Lightning components as well. Just add the Log class methods to any Apex application.
Download
I hope this component will be a great asset in your lightning component library! You can find the code on GitHub but we can also help with your coding and custom development needs.
Happy Coding!