logo

NJP

World of Client-side Scripts: #4 (Async, Debugging and Testing)

New article articles in ServiceNow Community ยท Oct 08, 2026 ยท article

Introduction

In #2 I said "minimize server lookups" and "prefer asynchronous GlideAjax." Many of you asked: how exactly? And when a client script misbehaves, how do I find out why? In this part, let us get hands-on with the patterns that keep forms fast, the tools that help you debug, and the newer ways to test client scripts โ€” including what the Australia release brings.

1. Getting data to the client: pick the right tool

NeedRecommendedAvoid

Data needed at form load

|

Display Business Rule + g_scratchpad

|

onLoad GlideAjax / GlideRecord

|
|

Data needed on field change

|

Async GlideAjax with getXMLAnswer()

|

getXMLWait(), client GlideRecord

|
|

One field from a referenced record

|

g_form.getReference() with a callback

|

getReference() without callback

|
|

Show / hide / mandatory / read-only only

|

UI Policy

|

Client script

|

2. g_scratchpad: load once, use anywhere

If the data is known when the form loads, fetch it on the server before the form reaches the browser. Zero extra round-trips.

// Display Business Rule on incident
(function executeRule(current, previous) {
    g_scratchpad.isVip = current.caller_id.vip == true;

    var ga = new GlideAggregate('incident');
    ga.addQuery('parent_incident', current.getUniqueValue());
    ga.addActiveQuery();
    ga.addAggregate('COUNT');
    ga.query();
    g_scratchpad.openChildCount = ga.next() ? parseInt(ga.getAggregate('COUNT'), 10) : 0;
})(current, previous);

// onLoad client script
function onLoad() {
    if (g_scratchpad.isVip) {
        g_form.addInfoMessage(getMessage('Caller is a VIP. Please prioritise.'));
    }
    if (g_scratchpad.openChildCount > 0) {
        g_form.showFieldMsg('state', g_scratchpad.openChildCount + ' open child incident(s)', 'info');
    }
}

3. Asynchronous GlideAjax done right

The client-callable Script Include:

var IncidentClientUtils = Class.create();
IncidentClientUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    getCallerDetails: function() {
        var callerId = this.getParameter('sysparm_caller_id');
        var user = new GlideRecord('sys_user');
        if (!user.get(callerId)) {
            return '';
        }
        return JSON.stringify({
            location: user.getValue('location'),
            vip: user.getValue('vip') == '1'
        });
    },
    type: 'IncidentClientUtils'
});

The onChange client script:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        return;
    }
    var ga = new GlideAjax('IncidentClientUtils');
    ga.addParam('sysparm_name', 'getCallerDetails');
    ga.addParam('sysparm_caller_id', newValue);
    ga.getXMLAnswer(function(answer) {
        if (!answer) {
            return;
        }
        var details = JSON.parse(answer);
        g_form.setValue('location', details.location);
        if (details.vip) {
            g_form.showFieldMsg('caller_id', getMessage('VIP caller'), 'info');
        }
    });
}

Why getXMLAnswer()? It hands you the answer string directly, so there is no XML parsing in your callback (getXML vs getXMLAnswer). Return several values as one JSON string instead of making several calls.

Security tip: a client-callable Script Include can be called by any logged-in user. Validate inputs and check access inside the method; never trust the client.

4. The onSubmit problem

Async calls and onSubmit don't mix naturally: the form submits before the callback returns. getXMLWait() freezes the browser and is not supported on Portal or Workspace. The common pattern is to stop the submit, validate asynchronously, and resubmit with a flag (How To: Async GlideAjax in an onSubmit script๐Ÿ˜ž

function onSubmit() {
    if (g_scratchpad._validated) {
        return true;
    }
    var actionName = g_form.getActionName();
    var ga = new GlideAjax('IncidentClientUtils');
    ga.addParam('sysparm_name', 'canSubmit');
    ga.addParam('sysparm_id', g_form.getUniqueValue());
    ga.getXMLAnswer(function(answer) {
        if (answer === 'true') {
            g_scratchpad._validated = true;
            g_form.submit(actionName);
        } else {
            g_form.addErrorMessage(getMessage('Validation failed.'));
        }
    });
    return false;
}

Better still, ask whether the check belongs on the server (a before Business Rule with current.setAbortAction(true)) or in a data policy. Server checks also protect imports, integrations and REST calls.

5. Debugging client scripts

  • Browser DevTools (F12): Console for errors, Sources for breakpoints, Network to see each xmlhttp.do GlideAjax call and its payload.
  • jslog('message') writes to the JavaScript Log; console.log() works in all UIs, including Workspace.
  • JavaScript Log and Field Watcher (Settings > Developer in classic UI): Field Watcher shows which client script, UI Policy or Data Policy touched a field.
  • Response Time indicator on the classic form: shows how long client scripts and UI Policies took. Use it after adding a new script.
  • Add a temporary debugger; statement to pause execution exactly where you need. Remove it before the update set moves.
  • Suspect a conflict? Temporarily set Active = false on scripts one by one, or check order and Global/Inherited settings from #1.

6. Testing client scripts with ATF

ATF can open a form, set field values and assert field states, which is exactly what client scripts change. Use "Open a New Form", "Set Field Values", and "Field State Validation" / "Field Values Validation" steps. For Portal, use the Service Catalog in Service Portal step set.

New in the Australia release: ATF Code Coverage highlights the lines your tests executed in green and the missed lines in red, and it covers client scripts, business rules, script includes and UI actions. Enable it with the system property sn.code.coverage.enabled = true; no extra licence is needed (Agentic ATF brings Code Coverage). Australia also adds an ATF troubleshooting agent that suggests the root cause of failed tests (Australia for developers).

One upgrade note from the community: client-side ATF steps need an active Client Test Runner. If an upgrade run fails on a client step, check the runner first (community thread).

7. AI-generated client scripts: review them like any other code

Now Assist for Creator and the Australia Build Agent can generate scripts for you, and with the ServiceNow SDK you can even build from external IDEs. That's a big productivity boost. But the generated code still has to pass the checks from this series:

  • Uses isLoading and empty newValue checks
  • Async GlideAjax, no client GlideRecord or getXMLWait()
  • No DOM or jQuery
  • Correct UI Type for where it runs
  • Could it be a UI Policy instead?
  • Covered by an ATF test

A note on modern Javascript: the ES12 (ECMAScript 2021) mode introduced in Tokyo applies to server-side scripts. Client scripts run in the user's browser, so check what syntax your instance's script editor accepts before using arrow functions or let/const in a client script.

Conclusion

Fast client scripts are about timing: load data once with g_scratchpad, fetch on change with async GlideAjax, and leave hard validation to the server. Then make your scripts provable โ€” debug with the right tool, and let ATF (now with code coverage) tell you what is really tested.

 

That doesn't wrap up the "World of Client-side Scripts" series, so stay tuned as there are more coming soon!

 

 

Mark it Helpful if it helped, and tell me in the comments which topic you would like next.

 

Thank You!

Regards,

Kailash Bhange,

LinkedIn

View original source

https://www.servicenow.com/community/developer-articles/world-of-client-side-scripts-4-async-debugging-and-testing/ta-p/3608670