Building Robust Error Handling in ServiceNow: A Layered Approach

Posted by admin on November 07, 2024

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.

Understanding the Full Stack Layers in ServiceNow

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:

  1. Portal Widget - The user interface layer in the Service Portal, designed with a focus on displaying user-friendly error messages.
  2. AngularJS Services - Client-side business logic where asynchronous errors (e.g., REST API calls) are managed and propagated to the UI.
  3. REST API or GraphQL Resolvers - Server-side interfaces that expose business functions and enforce consistent error-handling responses.
  4. Script Includes (Repository and Utility) - Backend logic:
    • Repository - Data access, following a modified repository pattern.
    • Utility - Encapsulates reusable, application-wide functions, particularly for non-CRUD operations.

The Goals of a Layered Error-Handling Strategy

When designing an error-handling strategy in ServiceNow, we aim to:

  1. Provide meaningful, user-friendly feedback at the front end.
  2. Log critical information centrally for troubleshooting.
  3. Propagate errors intelligently up through layers, adding context at each level.
  4. Display error codes to end users so they can report issues accurately, aiding IT in identifying and resolving problems faster.

Designing Error Handling for Each Layer

Portal Widget (Front-End)

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();
};

AngularJS Services (Angular Providers - Front-End)

In AngularJS services, handle asynchronous errors from REST calls. Here, errors are often caught in .catch() blocks. It's essential to:

  • Log errors locally using $log.error or a centralized logging API.
  • Extract and Bubble Error Codes: Retrieve error codes from API responses and pass them up to the widget for display.
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
    };
}

Scripted REST API

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.

  • Standardize Error Codes: Define a consistent format for error codes, e.g., [Module][ErrorType][ID].
  • Return Error Codes in Responses: Include error codes in the error message that the API sends back, so they’re available to the client.
(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);
Script Includes - Repository and Utility Layers (Data Layer)

The backend Script Includes manage data access and reusable functions. Each data-access Script Include can be enhanced to:

  • Bubble Up Contextual Errors with Codes: Each error is enriched with context and passed up to the resolver, enabling meaningful error information in the response.
  • Leverage a Centralized Error Logger: Use a logging Script Include (e.g., Logger) to record all errors to a custom syslog table or an external logging system.
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'
});

Centralized Error Logger with Stack Trace

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'
};

Conclusion

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.

Copyright © Formcloud LLC 2024