Best Practices for Coding in JavaScript
Organizing and Formatting Code
• Write code logic at the top and helper functions at the end.
• Declare global variables at the top.
• If you have more than 150 lines in your service, divide your code into functions or multiple services.
• Name your variables and functions appropriately to reduce the number of comments, and improve readability of code.
• Use the in-built JavaScript auto-format functionality in ThingWorx Composer.
Writing Code
• Use truthy and falsy cases in conditional statements.
// *** will act like a false statement ***
if (false)
if (null)
if (undefined)
if (0)
if (-0)
if (0n)
if (NaN)
if ("")
// *** will act like a true statement ***
if (true)
if ({})
if ([])
if (1)
if ("0")
if ("false")
if (new Date())
if (-1)
if (12n)
if (3.14)
if (-3.14)
if (Infinity)
if (-Infinity)
// *** you can force it into a boolean value by using the !! operator ***
// i.e.:
var myNullValue = null;
var myBooleanValue = !!myNullValue; // it will store as false
• Perform batch CRUD operations to minimize your interactions with the database. However, if you receive too much information from the database, then you might want to make multiple single calls to the database.
// *** bad practice ***
for (let i = 0; i < Things["thingName"].GetPropertyDefinitions().length; i++) {
var newEntry = new Object();
newEntry.name = Things["thingName"].GetPropertyDefinitions().rows[i].name;
newEntry.description = Things["thingName"].GetPropertyDefinitions().rows[i].description;
result.AddRow(newEntry);
}
// GetPropertyDefinitions() is called multiple times because of the looping
// *** preferred practice ***
let propertyDefinitions = Things["thingName"].GetPropertyDefinitions();
propertyDefinitions.rows.toArray().forEach((propertyDefinition) => {
result.AddRow({
name : propertyDefinition.name,
description : propertyDefinition.description
});
});
// By storing the content of GetPropertyDefinitions() ahead of time, you will prevent many unnecessary REST calls.
• Avoid manipulating Thing properties directly as this can result in persistent synchronization issues and loss of performance.
// *** common practice ***
var referencedInfoTable = Things["thingName"].myInfoTable;
for (let i = 0; i < 10; i++) {
var row = new Object();
row.field1 = i;
row.field2 = i * 2;
row.field3 = i * 3;
referencedInfoTable.AddRow(row);
}
Things["thingName"].myInfoTable = referencedInfoTable;
// the .AddRow() will implicitely write to cache, thus causing some performance issues
// *** preferred practice ***
var deepCloneInfoTable = Things["thingName"].myInfoTable.clone();
for (let i = 0; i < 10; i++) {
deepCloneInfoTable.AddRow({
field1 : i,
field2 : i * 2,
field3 : i * 3
});
}
Things["thingName"].myInfoTable = deepCloneInfoTable;
// the .AddRow() of the cloned infotable does not have a reference to the original infotable, therefore does not write to cache.
// *** for your information ***
var referencedInfoTable = Things["thingName"].myInfoTable;
var tableLength = referencedInfoTable.rows.length;
for (let i = 0; i < tableLength; i++) {
referencedInfoTable.rows[i] = {
field1 : i,
field2 : i * 2,
field3 : i * 3
};
}
Things["thingName"].myInfoTable = referencedInfoTable;
// because you are not using .AddRow() it doesn't require access to the referenced object's .AddRow() method, therefore it doesn't need to access the cache.
• To suppress linting on the next line, add a comment. This bypasses the linting errors that could only be addressed by fixing the linting library and still get linting everywhere else.
Objects
◦ Create objects using {} instead of new Object();. Try to create your object values inline because it is easier to read and faster to write.
// *** old practice ***
var myObject = new Object();
myObject.item1 = "value1";
myObject.item2 = "value2";
myObject.item3 = "value3";
// *** preferred practice ***
var myObject = {
item1:"value1",
item2:"value2",
item3:"value3"
};
◦ For deep cloning objects, use JSON.parse(JSON.stringify(object)).
Variables
◦ Declare your variables by using let rather than var. Use const to declare constant variables.
Using let prevents you from redeclaring and overwriting your variables. This also prevents the creation of functions with the same name.
// *** common mistake ***
var value = 1;
if (true) {
var value = 2;
}
// value = 2;because it was overwritten by `value` inside if statement
// *** how to avoid mistake ***
let value = 1;
if (true) {
let value = 2;
// value = 2; inside the if statement
}
// value = 1;because the `value` inside if statement does not affect the first `value`
// *** you can still access the variable in the outter scope ***
let value = 1;
if (true) {
value = 2;
}
// value = 2;because the content of `value` was replaced
◦ Use camel case to name variables.
◦ To set a default value for a variable for an undefined or null value, use the || (OR) operator.
// *** traditional way ***
var myNullValue; //undefined
if (myNullValue == null || myNullValue == undefined || myNullValue == "") {
myNullValue = "something";
}
var output = myNullValue;
// *** alternate way with ternary operator ***
var myNullValue; //undefined
var output = (myNullValue == null || myNullValue == undefined || myNullValue == "") ? "something" : myNullValue;
// *** preferred practice ***
var myNullValue; //undefined
var output = myNullValue || "something";
|
This may not be an ideal way to set a default value because it uses a falsy statement. For example, if you have a 0 value, the || operator might set another value instead of 0.
|
◦ Use dot notation to access variables. Use bracket notation only when you have special characters in your key, for example: object.row["field name with space"].
◦ Create a variable to store a Thing and use this variable in a loop.
// *** common mistake ***
for (let i = 0; i < 100; i++ ){
Things["thingName"].myProperty;
}
// *** best practice practice ***
let myThing = Things["thingName"];
for (let i = 0; i < 100; i++){
myThing.myProperty;
}
Functions
◦ Use camel case to name functions.
◦ Declare your functions as function declarations and not as variables.
You cannot move function expressions and function arrow expressions to the bottom of your code.
// *** function declaration (Preferred practice) ***
function doSomething1 (param) {
return param;
}
// *** function expression (Avoid) ***
const doSomething2 = function (param) {
return param;
}
// *** function arrow expression (Can be used inside .map(), .redude(), .filter()) ***
const doSomething3 = (param) => {
return param;
}
◦ Leverage return in functions to interrupt the flow of code logic. This helps is saving some CPU operations and promote readability.
// given that we have multiple validations, you can save some operations by leveraging a function return
// given dice1 = undefined, dice2 = null
// common way
let outputMessage;
if (dice1 > 0 && dice1 < 7) {
if (dice2 > 0 && dice2 < 7) {
if (dice1 + dice2 == 7) {
outputMessage = SUCCESS;
} else {
outputMessage = ERROR_NOT_SEVEN;
}
} else {
outputMessage = ERROR_BAD_DICE2;
}
} else {
outputMessage = ERROR_BAD_DICE1;
}
result = outputMessage;
// preferred way
result = rollSeven(dice1, dice2);
function rollSeven (dice1, dice2) {
if (!(dice1 > 0 && dice1 < 7)) {
return ERROR_BAD_DICE1;
}
if (!(dice2 > 0 && dice2 < 7)) {
return ERROR_BAD_DICE2;
}
if (dice1 + dice2 != 7) {
return ERROR_NOT_SEVEN;
}
return SUCCESS;
}
// the preferred way promotes readability. By using a return statement and only testing for failing cases it reduces the need for nested if statements.
Services
◦ Use Pascal case to name services.
◦ Use an inline parameter to call services. However, there if the input parameters are big or if you wish to reuse the parameters, store the input parameters as a variable, and then pass the variable into the service call.
// *** old practice ***
var params = {
"param1" : "paramValue1",
"param2" : "paramValue2"
}
Thing["thingName"].CallServiceName(params);
// *** preferred practice ***
Thing["thingName"].CallServiceName({
"param1" : "paramValue1",
"param2" : "paramValue2"
});
◦ Avoid using services that call the database inside a loop.
Infotables
◦ Use the following method to create an infotable:
// common way
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
// easier way
var table = DataShapes["GenericStringList"].CreateValues();
◦ Use the following method to get the length or row of an infotable:
// fetching a row
myInfoTable[i] = myInfoTable.rows[i] = myInfoTable.getRow(i)
// getting the length
myInfoTable.length = myInfoTable.rows.length = myInfoTable.getRowCount()
◦ Use the dot notation to access the data of the first row of the infotable:
// if you have an infotable with rows
// classical way to access the first row field
myInfoTable.row[0].field
// trick to access the first row field
myInfoTable.field
◦ Use array methods, such as .map(), .reduce(), .filter(), .some(), .every(), to manipulate infotables or to make changes to multiple infotable rows. You can use these methods for infotables if you use infotable.rows.toArray().[map() | reduce() | filter()].
Using .map():
// *** What you have ***
var officers = [
{ id: 20, name: 'Captain' },
{ id: 24, name: 'General' },
{ id: 56, name: 'Admiral' },
{ id: 88, name: 'Commander' }
];
// *** What you need ***
//[39, 44, 57, 95]
var officersIds = officers.map((officer) => officer.id);
result = officersIds;
Applying changes to multiple infotable rows:
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
table.AddRow({item:1});
table.AddRow({item:2});
table.AddRow({item:3});
table.AddRow({item:4});
Array.from(table.rows).map(row => {row.item = row.item * 2 + "hello";});
result = table;
Using .reduce():
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
table.AddRow({item:1});
table.AddRow({item:2});
table.AddRow({item:3});
table.AddRow({item:4});
// total sum should be 1 + 2 + 3 + 4 = 10
result = Array.from(table.rows).reduce((total, row) => total + parseInt(row.item), 0);
◦ To iterate and loop through data in infotables, use a .forEach() loop. This improves performance.
table.rows.toArray().forEach(row => {
data += row.item;
});
◦ For deep cloning infotables, use var deepClonedInfoTable = myInfoTable.clone().
Logger messages
◦ In your try … catch statements, name your exception object as err.
try {
Things["thingName"].CallServiceName();
} catch (err) {
logger.error(err);
} finally { // finally is optional
// will go here whether an exception occured or not
}
|
Logging the error (err in this case) into a JSON format is not recommended and err.message must be used instead as a best practice.
|
◦ Whenever you create a Thing, always surround it with a
try catch statement and delete it in case of failure in the
catch statement. This eliminates the creation of ghost entities. If you have ghost entities in your system, see
Ghost Entity Services to delete your ghost entities.
Whenever you have a service that creates an entity, make sure you handle a ghost entity use case.
try {
// Successfully creates the GhostThing entity.
Resources["EntityServices"].CreateThing({
name: "GhostThing" /* STRING */,
description: "Ghost" /* STRING */,
thingTemplateName: "GenericThing" /* THINGTEMPLATENAME */
});
} catch (err) {
// If an exception is caught, we need to delete the entity
// that was created to prevent it from becoming a Ghost Entity
Resources["EntityServices"].DeleteThing({
name: "GhostThing" /* THINGNAME */
});
}
◦ If you have any error logs that you want to display in the Monitoring script logs, include information such as the Thing name, the service name, line number, and the original error message. Keep in mind that you can use the {} as a placeholder for string formatting in your log. Be aware that the formatted message can reveal unnecessary information that might lead to security issues. Avoid showing too much detail if you are dealing with services that would be used in a Mashup or external systems.
The following properties are available in an exception object:
▪ err.lineNumber—Line number where the exception was thrown.
▪ err.name—Exception type, for example, JavaException
▪ err.fileName—Service name from which the exception was fired.
▪ err.message—Exception message.
try {
// error is fired here
} catch (err) {
let errMsg = "Thing [{}] Service [{}] error at line [{}] : {}";
logger.error(errMsg, me.name, err.fileName, err.lineNumber, err);
throw err; // throw original error message
}
// log output => Thing [PTC.SCA.SCO.MyThing] Service [CallMyService] error at line [2]: TypeError: Cannot find function CallMyService in object com.thingworx.things.MyThing@251c8397.
◦ Depending on your granularity needs, use the proper logger level.
Write loggers only when necessary. Too many log messages make debugging impossible.
Level
|
Description
|
DEBUG
|
Specify fine-grained informational events that are most useful to debug an application.
// for example, show the length of time a function takes to run. let startTime = new Date().getTime(); // do something let endTime = new Date().getTime(); let time = endTime - startTime; logger.debug("How long did it take to run something (ms) : {}", time);
|
INFO
|
Specify informational messages that highlight the progress of the application at a coarse-grained level.
// for example, you wish to track the progress of your service logger.info("Step 1 - creating variable"); let step1 = "do something"; logger.info("Step 2 - run a function"); myFunctionDoSomething(step1);
|
WARN
|
Specify potentially harmful situations.
// for example, you see a potential issue here let resultFromService = Things["thingName"].CallServiceName(); if (resultFromService == undefined) { logger.warn("My variable is undefined... strange"); } myFunctionDoSomething(resultFromService);
|
ERROR
|
Specify error events that might still allow the application to continue running.
// for example, a service was not suppose to throw an error try { var resultFromService = Things["thingName"].CallServiceName(); } catch (err) { logger.error("Something really bad happened: {}", err); }
|