- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
05-29-2023 01:45 AM - edited 05-29-2023 04:00 AM
In the several thousand questions I've answered here in the community, as well as in my customer projects over the last few years, I've seen a lot of JavaScript code. And in most cases, the developers implement only the so-called "happy path" but do not consider all the pitfalls and error cases which also have to be handled. This can lead to unexpected behaviors and errors that are difficult to find and, in the worst case, to broken functionalities and corrupted data. For this reason, I would encourage the reader of this article to invest more time in hardening the code to make it more fault-tolerant and robust. |
|
What is a "Happy Path" in the context of developing software?
On Wikipedia, you can read the following about the term "Happy Path":
In the context of software or information modeling, a happy path (sometimes called happy flow) is a default scenario featuring no exceptional or error conditions. For example, the happy path for a function validating credit card numbers would be where none of the validation rules raise an error, thus letting execution continue successfully to the end, generating a positive response.
As a software developer, you'll surely know that elation when, after hours or days of intensely writing code and putting all the necessary pieces together, your system or feature does what it's supposed to do. The path to the initial achievement of the goal is the "Happy Path", and in principle this is not bad, because it is what makes software development so exciting and exhilarating. However, an inexperienced software developer will stop at this point, because the goal has been reached. For the experienced software developer, however, this is where the real work starts, which from now on is no longer any fun.
Because the "Happy Path" is not a straight path but a possible route along many forks in the road, at each of which an explicit decision has always been made. Now the task is to walk the Happy Path once again and to take the routes you left before.
Applied to software development, this means trying to consider all possible scenarios that deviate from the "Happy Path" and then incorporating appropriate handling into the code. At this point, at the latest, it becomes clear how essentially an error handling concept is.
I think the diagram above makes it clear what a happy path is and that there are branch-offs that need to be handled to harden the code against unwanted input, events, or user actions.
But what does "Known unknown" and "Unknown unknown" mean, and what is the difference?
Known unknown
The set "Known unknown" stands for everything that the developer and/or the requester can think of in terms of undesired routes and which can therefore be handled explicitly. In the simplest case, the exact opposite of what belongs to the happy path is always considered at the decision nodes. Often these criteria also result directly or indirectly from the acceptance criteria of a story or other input sources such as concept documents, regulations, laws, etc., or can be derived by the developer from his wealth of experience.
Unknown unknown
This is the problematic set of error possibilities, which includes everything that could not be taken into account due to the lack of knowledge of all parties involved. Nobody is all-knowing and yet the source code must also be able to deal cleanly with unexpected states, events or user input.
Implementation Approaches
For implementing the "Happy Path" along with the validations of routes which lead to the "Unknown" worlds I see two general approaches.
Follow the breadcrumbs
I see the first approach most often with other developers and I have practiced it myself for many years. The basic idea is to check the positive conditions and only to continue if they calculated as true. As a result you have many nested if blocks:
|
As long as these nested if blocks span a few lines, this is not a problem, since you can see everything at a glance. In practice, however, you end up with very long if blocks that require constant scrolling to understand how you got to this point of the happy path.
Another problem is that the text indentation is increased with every additional nested if block, effectively leaving less and less space for the source code. Unsightly line breaks are then the result, which make understanding the code extremely difficult.
Immediately leave the room
In this second approach you have to reverse the conditions to always test the opposite of the positive case. In the event that a negative condition is met, further execution of the source code is stopped immediately - in most cases by leaving the corresponding method and returning to the caller.
|
In this approach all if blocks are decoupled thus allowing to rearrange them without breaking the code. And the "Happy Path" is now straight forward without any text intendations.
Transfer to a real example
In my current side project, I'm developing an application which has some features that can be switched on/off by an administrator via a user interface.
As a result of clicking on a slider in the UI, a GlideAjax call is made and in the related Script Include:
- the related system property representing the feature has to be loaded,
- the JSON-based value has to be parsed,
- the "is active" property has to be toggled and
- the system property has to be updated with the new JSON payload.
First naive version
The following code is fully functional and does exactly what it is supposed to do:
toggleFeature: function() {
var _strSysID = String(this.getParameter('sysparm_sys_id'));
var _isActive = String(this.getParameter('sysparm_active')) == 'true';
var _grProperty = new GlideRecord('sys_properties');
_grProperty.get(_strSysID);
var _objValue = JSON.parse(_grProperty.getValue('value'));
_objValue.feature_active = _isActive;
_grProperty.setValue('value', JSON.stringify(_objValue, null, ' '));
_grProperty.update();
},
After playing around in the UI Page which provides the user interface for the feature toggling, it stopped working. Since there were no error outputs in the syslog table, I tried in vain for a while to find the cause and thus fell fully into the "Unknow unknown" trap.
Next, I completely wrapped my code with a try-catch block so that any errors would be caught and written to the syslog table (also read my article DevBasics: Catch me if you can).
And finally I got an error message in the syslog table:
TypeError: Cannot set property "feature_active" of null to "true"
Apparently, on the following code line, the execution is broken.
_objValue.feature_active = _isActive;
But why was the code failing suddenly? If you ask yourself such questions without having an answer, you have not done a good job!
New & more sophisticated version
Therefore, one of the most important improvement measures that can be implemented with little effort is to output the values from the passed parameters at the beginning of the method:
toggleFeature: function() {
try {
var _strSysID = String(this.getParameter('sysparm_sys_id'));
var _isActive = String(this.getParameter('sysparm_active')) == 'true';
gs.info(
'Entering "toggleFeature()" with _strSysID = "{0}" and _isActive = "{1}"',
[_strSysID, _isActive]
);
This way I could see that the issue was on the client side, because in the GlideAjax call the Sys ID of the system property was not passed anymore:
Entering "toggleFeature()" with _strSysID = "null" and _isActive = true
For a beginner (and even for many experienced developers) this is an important moment of realization, because it becomes clear that you can never be sure that the expected input parameters are actually passed and are also valid ("Known unknown").
Before I present which error handling must be implemented, I would like to direct your attention to the value "null", because a trap from the area "Unknown unknown" hides itself here.
Since all values are transferred as text characters from the client to the server, the expectation is that the call to this.getParameter() will return a value of type string. Unfortunately the return value is of type object and many developers make the mistake here to force a string value using the toString() method. An in case a parameter is not available this.getParameter() returns "null" and an invocation of toString() on "null" would end in an interrupted execution with the runtime error message:
TypeError: Cannot convert null to an object.
For this reason, I already made an optimization by utilizing an explicit type conversion with String(). But we also have to be prepared to "null" values. This can be achieved by using the OR condition (double pipes "||") and assigning an empty string in case the expression left of the two pipes is representing undefined or a "null" value:
var _strSysID = String(this.getParameter('sysparm_sys_id') || '').trim();
var _isActive = String(this.getParameter('sysparm_active') || '').trim() === 'true';
Now it's time to implement the first check for valid input parameters and I guess that many of you would implement the check for the Sys ID similar to the following pattern:
try {
if (_strSysID.length === 32) {
//continue with execution
}
As already discussed, going forward that way we would end up in many nested if-else branches, which are difficult to read and maintain.
Therefore, the better approach is to check always the opposite of a certain condition and to stop the execution by returning the method within the if branch:
try {
if (_strSysID.length !== 32) {
return;
}
//continue with execution
That way, the text indentation of the following parts along the "Happy Path" remains the same and thus is better to read and understand.
But just returning a Script Include method which is invoked by a GlideAjax request from the client side is not helpful as no feedback is returned to the client. And while a method by its nature can only return a single value, in practice you often need several, for example a flag that indicates whether the call was successful or not and a text that provides the reason.
For this reason, I use a pattern which allows responding several dedicated values back to the client while returning the method. It's a small embedded function, which builds an object with two properties successful & message. And don't forget to transform that object with JSON.stringify() to a text-based representation because that is the only data format that can sent back to the client:
toggleFeature: function() {
var _getReturnValue = function(wasSuccessful, strMessage) {
var _wasSuccessful = wasSuccessful || String(wasSuccessful) === 'true';
var _strMessage = String(strMessage || '').trim();
var _objReturnValue = {
successful : _wasSuccessful,
message : _strMessage
};
if (!_wasSuccessful) {
gs.error(_strMessage);
}
return JSON.stringify(_objReturnValue);
};
Now, the first validity check can be implemented as follows:
try {
if (_strSysID.length !== 32) {
return _getReturnValue(
false,
'Value "' + _strSysID + '" of parameter "sysparm_sys_id" does not represent a Sys ID!'
);
}
The client then can decide whether to display the error message to the user or just to inform about a failing operation.
The next steps on the "Happy Path" are further validity checks with the respective reactions:
var _grProperty = new GlideRecord('sys_properties');
if (!_grProperty.get(_strSysID)) {
return _getReturnValue(
false,
'There is no feature property available for Sys ID = "' + _strSysID + '"!'
);
}
var _strPropertyName = String(_grProperty.getValue('name') || '').trim();
var _strValue = String(_grProperty.getValue('value') || '').trim();
var _objValue = null;
if (_strValue.length === 0) {
return _getReturnValue(
false,
'System property "' + _strPropertyName + '" does not contain any value!'
);
}
try {
_objValue = JSON.parse(_strValue);
}
catch (e) {
return _getReturnValue(
false,
'System property "' + _strPropertyName + '" does not contain valid JSON data!'
);
}
if (typeof _objValue.feature_active !== 'boolean') {
return _getReturnValue(
false,
'In the JSON-based value of system property "' + _strPropertyName +
'" the property "feature_active" is missing or is not of type "Boolean!"'
);
}
In the above script, you can see that JSON.parse() is wrapped in an individual try-catch block. This is the ultimate safety net for all possible errors and problems that can arise from converting text in a JSON notation to a JavaScript object ("Unknown unknown").
Up to this point, only the necessary preparations have been made so that the flag "feature_active" can be toggled, which is done with one line of code finally. I also check whether the GlideRecord update was successful or not (if (_grProperty.update() === null)) - something many developers like to forget:
_objValue.feature_active = _isActive;
_grProperty.setValue('value', JSON.stringify(_objValue, null, ' '));
if (_grProperty.update() === null) {
return _getReturnValue(
false,
'System property "' + _strPropertyName + '" could not be updated!'
);
}
return _getReturnValue(
true,
'Feature "' + _objValue.feature_name + '" could be ' +
(_objValue.feature_active ? 'activated' : 'deactivated') + ' sucessfully.'
);
Conclusion
In relation to the first naive but working approach, the new script version is extremely bloated, but it is worth the effort. The probability that the new code will fail is extremely low. The many individual validation blocks are easy to read and follow, and in my opinion do not need any additional code comments.
Furthermore, the new code with its unique return value for "successful" or not is very well-prepared to be checked by automated tests.
I hope I was able to show with my article that it is worth being unhappy, because only in this way will you leave the "Happy Path" to explore the dark worlds of the "Unknown", so that all possible and impossible error cases can be safely intercepted, and the user can be given concrete hints about what went wrong.
If you liked that article you maybe also want to read the ohter ones from my "DevBasics" series:
- 2,097 Views