- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
02-19-2023 09:26 AM - edited 05-29-2023 04:11 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 code was not secured against the occurrence of runtime errors. In a high-level programming language like Java, such runtime errors are called "exceptions" and in Java-based software development you will spend a considerable amount of time defining and implementing a clean exception handling concept. But in the ServiceNow world, I unfortunately miss this awareness. I therefore hope that with this article, I can encourage the reader to invest more time and attention in catching and handling runtime errors. |
|
Why is Error Handling important?
ServiceNow is an amazing platform and implementing something in ServiceNow is a lot of fun, because you can realize very sophisticated features with very little time or effort and even bring complete applications to life. An inexperienced ServiceNow developer (in my experience, unfortunately, most of them are) only has success in mind and therefore focuses exclusively on the so-called happy path that leads directly to success.
But many dangers lurk off the beaten path, such as those evil end users who simply don't do what's expected of them. Or external systems that do not adhere to the protocols and interface contracts.
An experienced developer knows all these dangers and will prepare and protect his code accordingly. Yes, all the required hardening measures cost something and hopefully these additional expenses have been calculated in. If not, the only hope is that nothing will go wrong, but in my experience this hope is always thoroughly destroyed.
In order to make source code or an entire application stable and fault-tolerant, it is therefore crucial to handle errors due to incompatible data, unexpected states or behaviors cleanly and to convert them into understandable reactions for the actors involved.
What is a Runtime Error?
Many issues can arise when running JavaScript code. These errors can be caused by incorrect input, programming faults, or other unforeseen circumstances. Already the call of an unknown function will result in raising a runtime error. In that case, no more code will be performed following the line that triggered the issue:
ℹ️ Note:
In contrast to the built-in Script Console (module "Scripts - Background") the tool XPlore used in the above example automatically allows a deep insight into the data structures that were output last in the script - in that case it was the generated Error object.
In ServiceNow, almost all JavaScript code is called in some methods, which in turn call other methods, creating a new execution layer each time. This stack of execution layers is known as call stack. If a runtime error occurs, the current code execution is aborted and an Error object is created (better known as "an error is thrown"), which is propagated to the next lower execution level of the call stack. If the runtime error is not caught there, the execution is also aborted at that level and the Error object propagates to the next lower execution level. This repeats until the Error object reaches the lowest execution level of the call stack, where, if left unhandled, it will finally "crash" the execution of the JavaScript code.
ℹ️ Note:
In the above screenshot the expected call stack when the Error occurred is highlighted and as you can see the complete call stack is even larger as below the level of the initial JavaScript execution there are more code levels. The output of a complete call stack is also referred to as a "stack trace".
Does this mean a runtime error can crash the ServiceNow instance?
No, fortunately this is not possible. The server-side JavaScript code is executed by the so-called Rhino engine, which was the first JavaScript engine developed to run in a non-browser environment. Introduced by NetScape in 1997, it was designed to run in a mixed-language environment where the host language is Java.
That engine creates for each script execution an individual context with predefined global variables, like gs, answer (ACLs) or current (Business Rules) and access to all the server-side APIs. Finally, the JavaScript code is loaded into this "sandbox" and executed. And in case of any runtime errors only that sandbox will crash but not the complete Java-based environment, the sandbox is embedded in.
Does this Rhino engine behave differently than a browser when runtime errors occur?
Unfortunately, yes as some of the possible runtime errors are suppressed.
To see the differences, you can open in your browser the console window from the developer tools (key F12 or an individual browser option) and enter some JavaScript there. Use SHIFT+ENTER if you want to add a new line, as hitting ENTER only will execute the code.
Server-side JavaScript code in ServiceNow:
Client-side JavaScript code in a browser:
As you can see in the screenshots above, ServiceNow does not throw a runtime error while the browser does.
How do I know whether any ServiceNow APIs might throw certain errors?
In principle, you should be able to expect this from the API documentation at developer.servicenow.com, but it is only mentioned there in very few cases. For example for the method setDisplayName() of the well-known GlideDateTime API:
And a test confirms that:
ℹ️ Note:
In the above example, not even a JavaScript error object is thrown but directly a java.lang.RuntimeException (an Exception is the Java counterpart of an Error object in JavaScript) from the underlying Java layers - in my opinion a severe design flaw.
However, for most of the API documentation, such important references are missing. A prominent example is instantiating a RESTMessageV2 object by specifying the record name from the sys_rest_message table and that of the HTTP method from the sys_rest_message_fn table to be executed. If you are using names which are not available, also in that case a Java-related Exception is thrown:
Can I create a custom Error type?
Yes, this is possible and sometimes even useful. The following example demonstrates how to implement such a custom Error class and how to throw it.
function MyError(message) {
this.message = message;
}
MyError.prototype =
Object.create(Error.prototype, {
constructor: {
name: "MyError",
value: Error,
enumerable: false,
writable: true,
configurable: true
}
});
throw new global.MyError('test');
You can store the complete code of the above custom Error object in a Script Include with name "MyError". Later in this article, I will go into more detail and show how to distinguish such a custom Error object from the native Error objects.
How to catch runtime errors?
The most important tool to make your JavaScript code robust and fault-tolerant is utilizing a so-called try{}-catch{} block with an optional finally{} block:
try {
// code that may throw an error
}
catch (e) {
// code to be executed if an error occurs
// in variable "e" the instance of the thrown Error is available
}
finally {
// code to be executed regardless of an error occurs or not
}
Within the try{} block, you can place your code as usual. In case any runtime error is thrown there, the JavaScript interpreter jumps right into the catch{} block and executes all the code contained in that block. The variable e (if required, you can also use another variable name) holds the instance of the thrown Error object and can be used to react accordingly, for example to perform any error logging. If there is an additional finally{} block, the code contained in it is executed in any case after the try or catch block has been processed. For example, you can place code in the finally{} block that sets once instantiated objects to null and thus frees valuable memory space
In the BPMN notation, the execution flow of the above pattern looks as follows:
A complete example could look like this:
var strRestMessageName = 'Google Maps API';
var strRestMessageMethod = 'setApiKey';
var objRestMessage = null;
try {
objRestMessage = new sn_ws.RESTMessageV2(strRestMessageName, strRestMessageMethod);
}
catch (e) {
gs.error(
'It was not possible to instantiate a RESTMessageV2 object with name "{0}" and method "{1}"!',
strRestMessageName, strRestMessageMethod
);
}
//something went wrong, so handle that error situation
if (objRestMessage === null) {
}
The syslog table now contains the following output:
I would like to add a few important notes to the above code example:
- Whatever variables are defined inside the try block, they are no longer available outside the block. That's why I defined them in line 1 and 2, so that their values can be used by the log output within the catch{} block.
- The code within the catch{} block is not protected against another runtime errors. Therefore, the code here should be reduced to the absolute minimum and implemented error-free.
- Variable e of the catch{} block is not used or examined further, as only one kind of error is to be expected.
- The error output to the syslog table within the catch{} block is actually of limited value, because you don't know where exactly in the code the output was written. However, this is generally the problem when using ServiceNow's own log output commands like gs.info() or gs.error(). That's the reason why I implemented my own log helper class. For more information, see my article Just another Log Helper
- Unfortunately, sometimes it is not possible to prevent further error logging in the lower execution layers. After executing the above code example, there is also a huge stack trace found in the syslog table:
How can all the Error types be identified and differentiated?
The only way to differentiate all Error types is utilizing the instanceof operator. This also applies to the custom Error type MyError introduced a chapter before.
The following example represents a simple approach for an error handling concept. Publicly available methods (checkValue()) internally forward to private methods (_checkValue()), which can throw different errors. The public method catches these errors and in turn throws a custom error (MyError()), which contains the error message from the lower layers of the call stack. The caller of the public method then catches the custom Error and can decide how to handle it:
While it is possible to explicitly throw errors in a method, I don't recommend doing so in ServiceNow. Unlike a Java-based program, where all code components are developed and compiled together in one IDE, in ServiceNow code fragments are spread across many tables and thus have no knowledge of each other. It gets even more complicated when different development teams work simultaneously in one instance. Any error handling concept, no matter how simple, would first have to be rolled out at great expense and communicated to all teams, which would never work in practice.
In my own projects, I instead follow the approach from the next chapter.
Is it possible to nest try{}-catch{} blocks?
Yes, it is, and with the help of this option I can implement my preferred error handling concept. In that concept, I make sure that no thrown Error will leave the current method by using an outer try{}-catch{} block, which catches everything that has not been handled before and thus acts as a safety net.
Within the outer try{} block, as much individual and topic-specific error handling as possible is performed. And at the end of the method, it must be ensured in every case that a defined value is returned. Even if the method has rather activity character ("do something") I got used to returning at least "true" (everything went well) or "false" (something went wrong).
function initializeAllSettings() {
try {
try {
// perform some code
}
catch (e) {
//decide whether this is required, if not proceed without leaving
return false;
}
try {
// perform some other code
}
catch (e) {
//decide whether this is required, if not proceed without leaving
return false;
}
}
catch (e) {
gs.error(
'Due to an internal error method "initializeAllSettings()" could ' +
'not finished successfully: ' + e.message
);
return false;
}
return true;
}
More DevBasics articles
If you liked that article you maybe also want to read the other ones from my "DevBasics" series:
- 5,844 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
One of the 'BEST' articles!
To be bookmarked! Thank you @Maik Skoddow
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Nice article, thanks for taking the time.