Error handling is a crucial part of development, especially on low-code platforms like ServiceNow, where it’s often overlooked, even in some of ServiceNow’s out-of-the-box modules. In consulting environments, both clients and vendors tend to expect quick turnarounds, fueled by the promise of “low-code/no-code” and fast, simple changes. While low-code can make certain adjustments easier, it still requires a critical mindset. Tight timelines and delivery expectations often lead to a “just get it to work” mentality, where robust error handling and other fundamentals are sidelined to meet short-term goals.
For complex applications, a robust error-handling mechanism is key to maintaining stability, providing clear diagnostics, and creating a seamless user experience. This post explores a structured approach to error handling in ServiceNow, focusing on layered architecture principles that scale error management across each layer, from front-end interfaces in the Service Portal to backend Script Includes.
Disclaimer: Before diving in, I want to clarify that I don’t claim to be an expert on error handling. What I’m sharing here is based on my own knowledge and experience, and I’m always learning. The code samples included in this post are just examples to illustrate key points and concepts, they may need adjustments to work. My goal is to provide a perspective on error handling that others might find useful, but these ideas should be tested and adapted to fit your own needs and applications.
Effective error handling requires a thorough understanding of the application's stack, as this enables us to design error-handling mechanisms tailored to each layer. In our architecture, we compartmentalize responsibilities and define distinct error-handling roles for each layer, ensuring a structured and resilient approach to managing errors:
When designing an error-handling strategy in ServiceNow, we aim to:
Portal Widgets form the user interface in the Service Portal. Error handling here is about user experience, presenting clear messages that don’t overwhelm the user and include an error code where possible.
Display Friendly Error Messages with Error Codes: Use spUtil.addErrorMessage() to provide the end user with an error message that includes a code they can share with support. This allows IT personnel to trace the error quickly.
api.controller = function($scope, TaskService, spUtil, $log) {
/* widget controller */
var c = this;
$scope.loadData = function() {
TaskService.getActiveTasks()
.then(function(response) {
$scope.data = response.data.result;
})
.catch(function(error) {
// Log error for debugging, including the stack trace
$log.error("Widget Error:", error);
// Display user-friendly error message with error code
var userMessage = error.code ?
"An error occurred while loading data. Please report this code to support: " + error.code :
"An error occurred. Please try again.";
spUtil.addErrorMessage(userMessage);
});
};
// Load data when the widget initializes
$scope.loadData();
};
In AngularJS services, handle asynchronous errors from REST calls. Here, errors are often caught in .catch() blocks. It's essential to:
function TaskService($http, $log) {
function getActiveTasks() {
return $http.get('/api/x_myapp/getActiveTasks')
.catch(function(error) {
$log.error("API Call Error:", error);
// Attach the error code to the original error
error.code = error.data?.error_code || "";
// Re-throw the original error to preserve the stack trace
throw error;
});
}
return {
getActiveTasks: getActiveTasks
};
}
In ServiceNow, the Scripted REST API serves as the Data API layer. This layer retrieves data from the backend (e.g., Repository Script Include), logs errors with unique error codes, and returns a simplified error message and code to the front end.
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
try {
var data = new TaskRepository()
.addEncodedQuery('active=true')
.getMultiple();
response.setStatus(200);
response.setBody(data);
} catch (e) {
var errorCode = e.errorCode || "";
var error = new sn_ws_err.ServiceError();
error.setStatus(500);
error.setMessage(`Unable to retrieve data. Error code: ${errorCode}`);
error.setDetail("An unexpected error occurred. Please contact support with the error code.");
return error;
}
})(request, response);
The backend Script Includes manage data access and reusable functions. Each data-access Script Include can be enhanced to:
var TaskRepository = Class.create();
TaskRepository.prototype = Object.extendsObject(Repository, {
initialize: function(tableName, overrideACL) {
this.errorLogger = new Logger();
var table = tableName || 'task';
Repository.prototype.initialize.call(this, table, overrideACL);
},
getActiveTasks: function(isSerialized) {
try {
this.addQuery('active=true');
return this.getMultiple(isSerialized);
} catch (e) {
var errorCode = this.errorLogger.generateErrorCode('TR', 'DATA_ACCESS');
this.errorLogger.logError(e.message, errorCode, 'TaskRepository.getActiveTasks', e.stack);
// Attach the error code to the original exception
e.errorCode = errorCode;
// Re-throw the original exception to preserve the stack trace
throw e;
}
},
type: 'TaskRepository'
});
The Logger Script Include is to accept and store stack traces in a centralized logging table. This keeps error information organized and easily accessible for debugging.
Note One common issue during development is that developers may add logging statements for debugging purposes and later forget to remove or disable them before code is deployed to production. This oversight can lead to sensitive or unnecessary information being logged in production environments, cluttering logs with development data and potentially exposing sensitive details. The Logger Script Include helps address this by centralizing logging functionality, allowing developers to log debug data using the logDebug method in lower environments. This centralized approach provides a "turn-off switch" for debug logging in production, ensuring that only essential logs, such as error and stack trace information, are recorded in live instances. By controlling logging through a central utility, we can keep production logs clean, secure, and relevant to production needs.
var Logger = Class.create();
Logger.prototype = {
initialize: function() {},
logError: function(message, code, context, stack) {
// Log the error here, ensuring that the error code and stack trace are included.
// For example:
gs.error(`[${code}] ${context}: ${message}\nStack Trace:\n${stack}`);
// Optionally, write to a custom logging table or external system.
},
logDebug: function(message, code, context, stack) {
// Include debug logging as needed, controlled by environment settings.
},
generateErrorCode: function(moduleCode, errorType) {
var nextId = gs.getProperty("x_your_app.last_error_id", "0");
nextId = parseInt(nextId) + 1;
gs.setProperty("x_your_app.last_error_id", nextId.toString());
return moduleCode + errorType + String(nextId).padStart(3, '0');
},
type: 'Logger'
};
A layered error-handling approach in ServiceNow ensures that errors are managed gracefully, from the front end through to the backend. With clear messages, unique error codes, centralized logging, and environment-specific debug capabilities, developers can deliver a seamless user experience while simplifying troubleshooting.