- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
03-19-2023 03:06 AM - edited 05-29-2023 04:14 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, implemented methods/function were not secured against their incorrect use. This can lead to errors that are difficult to find and, in the worst case, to corrupted data. I therefore hope that with this article, I can encourage the reader to invest more time and attention in hardening the so-called signatures of methods & functions. |
|
What are methods & functions in JavaScript?
Before I go into the signatures, I want to explain the terms "function" and "method" briefly.
Function | Method |
A JavaScript function is a block of code designed to perform a particular task. | A JavaScript method is an object property that has a function value. |
Definition:
|
Definition:
|
Example:
|
Example:
|
Special Functions: An Immediately-invoked Function Expression is a way to execute functions immediately, as soon as they are created. IIFEs are very useful because they don't pollute the global scope, and thus are a way to isolate variables declarations.
Definition:
A prominent example in ServiceNow can be found in Business Rules. Their predefined script code represents an IIFE. The two variables current and previous exist in the global scope and by passing them to the IIFE they cannot be changed within the function:
|
|
So basically, functions and methods represent the same concept - they just differ in the way how they are defined. For this reason, I will only use the term "functions" in the next chapters, but I will always refer to "methods" as well.
What are signatures?
The function signature consists of the function name followed by a comma-separated list of all parameters the function requires enclosed in brackets:
Is overloading possible in JavaScript?
In some languages like Java overloading of an function is an important concept. Overloaded functions have thte same name but different parameters like
doSomething(a) and doSomthing(a, b);
To invoke the second function you would have to pass two parameters. Unfortunately this is not possible in JavaScript as always the latest defined function is used:
In the above screenshot you can see that the second variant of a function doSomething() is used although the signature of the function call matches the first variant.
Approaches for hardening the signatures
JavaScript requires more caution from developers than other languages do. In high-level languages like Java, if you pass a function fewer parameters than it expects, a compiler error will be raised, but not in JavaScript. In the following example, a defined function is invoked in several variants without any issues:
In JavaScript, parameters are passed positionally. So, if the function signature specifies two parameters, JavaScript assumes the first value passed to the function is the first parameter as well as the second value passed to the function is the second parameter, and so on. But there is no type check. That means you can pass any values without problems. And if no values are provided, JavaScript will also not raise an error. The respective parameters are just left as "undefined", as you can see in the above screenshot.
This characteristic of JavaScript can lead to serious problems if the developer does not build his code in a way that missing parameters or wrong data types are caught and handled accordingly.
Use consistent naming conventions
Since JavaScript is quite flexible, many developers either misuse it or use it incorrectly, which could lead to messy and hard to understand code. Therefore, it is important to adhere to naming conventions to give the user of functions a clear indication of what data type and what kind of data is expected for each parameter.
For example, have a look at the following function signature:
function addUser(name, membership) {
}
Do you really know what kind of ata to pass for the parameters name and membership? Is it the first name, the last name, or even the full name of a user for the parameter name? Is a boolean value expected for the parameter membership, or maybe the number of years a user is already a member of something?
So there are two aspects to consider when naming parameter names: the data type and the semantics.
To make clear what data type has to be passed for a parameter, I recommend using the so-called Hungarian notation. But I'm not a fan of single letter prefixes like sName as this is not unique enough and furthermore hard to read. For string-based values I therefore prefer "str" as prefix, for example strName. And for boolean values, use prefixes which support the following parameter name, for example hasMembership or isMember.
An improved version of the above function signature can look like this:
function addUser(strName, isMember) {
}
ℹ️ Note:
If you want to learn more about naming conventions, I recommend reading my article DevBasics: Give me names
Now the data types are clear, but not what exactly is expected. Therefore, name the parameters as unambiguously as necessary and do not worry about their length:
function addUser(strFullName, isMemberOfCCC) {
// code
}
Add code documentation to explain the intended use
Although the function signature above is much better, the parameter names still leave some room for interpretation. This becomes even worse when numeric parameters come into play, for which only certain number ranges are valid. Here, only a verbal description helps to explain to the user of the function as precisely as possible what exactly is expected in terms of values for the individual parameters.
For this kind of inline code documentation, many high-level programming languages provide a tool that generates HTML documentations based on special annotations within code comments. For Java it is Javadoc and for JavaScript there is a corresponding ported version JSDoc, which offers almost completely the same annotations as Javadoc.
The following table lists some frequently used annotations:
Tag | Description |
@auhtor |
Developer's name |
@Param |
Documents a function parameter; a datatype indicator can be added between curly braces |
@returns |
Documents a return value |
@see |
Documents an association to another object |
@todo |
Documents something that is missing/open |
@throws |
Documents an exception thrown by a method |
@version |
Provides the version number of a library |
An example of code documentation using JSDoc annotations might look like this:
/**
* Creates a new user record in the internal repository.
*
* @param {String} strFullName
* Specify the full name of a user consisting of the first name and the last name
* separated by a blank char.
* @param {Boolean} isMemberOfCCC
* Expects `true`in case the user is member of the Chaos Computer Club, otherwise `false`.
* @returns
* `true` in case the user could be added to the repository successfully, otherwise `false`.
*/
function addUser(strFullName, isMemberOfCCC) {
}
Modern IDEs like Visual Studio Code are able to interpret the code comments and build nice looking popups when hovering with the mouse over the respective declarations:
Throwing Errors vs. Returning Error Status
Now the function signature is well understandable and extensively documented. But it cannot be prevented that the function is called incorrectly, for example by swapping or omitting parameters.
Therefore, we need an approach to enforce the correct usage. The most "painful" method JavaScript offers is throwing errors. Painful because a thrown error will cause the current script execution to abort if it is not caught properly.
Look at the following example. At the beginning of the function addUser() different checks make sure, that invocations are done with the right number of parameters and each parameter has the expected data type. And the function signature got another parameter numAge, which it is not used in the invocation. As a result, an Error is thrown:
While in high-level languages like Java this kind of error handling is commonplace, I see it as problematic in ServiceNow. In ServiceNow, there is no single program that is executed as a whole, but many different places where often only isolated function calls are made in single-line text fields (e.g. condition field in Business Rules). Here you cannot handle errors properly with the help of try-catch blocks.
ℹ️ Note:
If you want to learn more about throwing and catching errors, you can read my article: DevBasics: Catch me if you can
Another approach I prefer instead is indicating any issues via returning a defined error status. In this way, the caller of the function can decide for himself how to handle that situation.
For functions which just do anything (e.g. addUser()) you could return "false" in case the actions could not be performed, otherwise "true". For functions which are intended to return any value (e.g. getUser()) you can return "null" in case of any issues.
The following example was slightly modified to return "false" for invalid parameter values instead of throwing an error:
Parameter Guessing and Default Values
As already mentioned before, there are no fixed signatures in JavaScript. That means you can call functions with any number of parameters in any order. We can take advantage of this concept and adapt our function to make its use more fault-tolerant and stable.
For this approach, we can use within a function body a predefined variable with name arguments that holds the values of all passed parameters. The arguments object behaves like an array, as it has a length property and can be looped through like a regular array:
With that in mind it is possible to differentiate the individual parameter values by their type and relate them to the function signature.
Additionally you can support the developer who is using your function by assuming standard values for any left-out parameters. In a modern JavaScript engine you can define default values within the signature definition:
Currently, in ServiceNow this is only possible for client-side scripts as they are executed in the browser and for server-side scripts in scoped applications.
But there is a generic pattern for server-side scripts in ServiceNow, that can simulate such a feature for default values:
The magic is done by the OR operator || (two pipeline chars) within a value assignment. In case the expression left of the operator is 'undefined' the expression right of the operator is assigned.
You may wonder what added value the private function _addUser() provides. So with that pattern of a public function with a defined and well-documented signature you can simulate a kind of interface which is provided in high-level languages like Java.
And you can divide the work on a function between two developers. One takes care of the public method and its parameter processing as well as error handling and documentation of the signature. This way, it is possible to develop tests early and check whether the function is hardened enough to resist all eventualities.
Another developer, on the other hand, can fully concentrate on developing the business logic in the private function.
Use Data Objects instead of individual Parameters
Probably the safest and most elegant way to pass defined values to a function is to use data objects instead of many individual parameters. Since the values in an object can be referenced by unique names, the order of the values is irrelevant. Combined with the approach to define default values presented in the previous chapter, this variant is easy to maintain & robust.
Conclusion
I hope I was able to raise a little awareness of all the things that need to be thought about when implementing functions/methods in JavaScript that are understandable, but also robust and fault tolerant. And even though there are no fixed function signatures in JavaScript, it is important to sharpen and document them so that unnecessary errors due to misunderstandings can be avoided.
If you liked that article you maybe also want to read the other ones from my "DevBasics" series:
- 1,641 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thank you for the helpful article Maik!
Do you ensure the proper data is entered into the field via the use of an onChange Client Script or a Business Rule calling a Script Include? Is there some other way you handle form validation if improper data in entered into the field(s)?
Best regards,
Kevin