Overview — the debugging toolkit
ServiceNow's debugging tools split across two environments:
- Server-side tools — for Business Rules, Script Includes, Scheduled Jobs, REST API scripts, and anything that runs on the ServiceNow server: gs.log/gs.debug/gs.info/gs.warn/gs.error, Scripts - Background, the Script Debugger, and session debug logging
- Client-side tools — for Client Scripts, UI Policies with scripts, and UI Actions: browser developer tools console
Most ServiceNow debugging happens on the server side. Understanding the system log and Scripts - Background is where you will spend the most time.
gs.log() — your first debugging tool
gs.log() writes a message to the System Log, accessible at System > System Log > All. It is the equivalent of console.log() for server-side scripts.
// Basic logging
gs.log('Script is running');
gs.log('Current record state: ' + current.getValue('state'));
gs.log('Assigned to sys_id: ' + current.getValue('assigned_to'));
// With source identifier (highly recommended)
gs.log('Starting incident escalation check', 'IncidentEscalationBR');
gs.log('Count result: ' + count, 'IncidentEscalationBR');
// Logging objects as JSON
var data = { state: current.getValue('state'), priority: current.getValue('priority') };
gs.log(JSON.stringify(data), 'MyScript');
Always pass a source identifier as the second argument — it makes filtering the system log dramatically easier. Without a source, your log messages are indistinguishable from the thousands of other messages the platform generates.
Log levels — when to use each
// Use for normal diagnostic information during development
gs.log('Processing record: ' + current.sys_id);
// Use for expected but significant events
gs.info('Escalation triggered for incident: ' + current.number);
// Use for unexpected but non-fatal situations
gs.warn('Assignment group not found, defaulting to: ' + defaultGroup);
// Use when something has definitely gone wrong
gs.error('Failed to fetch user data for sys_id: ' + userId);
In production ServiceNow instances, debug log output is suppressed by default to protect performance. gs.log() and gs.info() are generally visible; gs.debug() output is only visible when session debug is enabled. For messages you need to see in production logs, use gs.log() or gs.error() depending on severity.
Reading the System Log
Navigate to System > System Log > All to see all log entries. The log can be overwhelming without filtering. Key filter techniques:
- Filter by Source — if you passed a source identifier to gs.log(), filter the Source column to that name
- Filter by Level — filter to Error to see only errors, Warning for warnings and errors combined
- Filter by Created — filter to the last 5 minutes or the time you ran your test
- Right-click filter — right-click any cell in the list and choose Filter to quickly add that value as a filter
For Business Rule debugging, the Source field typically shows the Business Rule name or the Script Include name. For background jobs, it shows the Scheduled Job name.
gs.debug() with session debug
gs.debug() messages only appear in the log when session debug is enabled. Session debug writes diagnostic messages for your current browser session without cluttering the system log for other users.
To enable session debug:
- Navigate to System Diagnostics > Session Debug > Log
- Click Enable
- Trigger your script (save a record, navigate to a page, etc.)
- Navigate back to System Diagnostics > Session Debug > Log to see the output
gs.debug('Checking user role: ' + gs.getUserID(), 'MyScript');
gs.debug('Query result count: ' + gr.getRowCount(), 'MyScript');
gs.debug('Entering complex validation block', 'MyScript');
Session debug is useful for tracing execution flow — adding multiple gs.debug() calls at key points to understand which code path executed and what values were present.
Scripts - Background — the most powerful debugging tool
Scripts - Background (System Definition > Scripts - Background) lets you run any server-side JavaScript interactively and see the output immediately. It is the fastest way to:
- Test GlideRecord queries and see what they return
- Test Script Include methods with specific parameters
- Verify that your encoded query logic matches the records you expect
- Prototype logic before putting it in a Business Rule
- Investigate production data without triggering Business Rules
// Test a GlideRecord query
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1^state!=6');
gr.setLimit(5);
gr.query();
while (gr.next()) {
gs.log(gr.number + ' | ' + gr.short_description + ' | ' + gr.assigned_to.getDisplayValue());
}
gs.log('Total P1 open: ' + gr.getRowCount());
// Test a Script Include method
var utils = new IncidentUtils('GROUP_SYS_ID', '1');
gs.log('Open P1 count: ' + utils.getOpenCount());
gs.log('Oldest open: ' + utils.getOldestOpen());
// Test a GlideAggregate
var ga = new GlideAggregate('incident');
ga.addEncodedQuery('active=true');
ga.addAggregate('COUNT', 'priority');
ga.groupBy('priority');
ga.query();
while (ga.next()) {
gs.log('P' + ga.getValue('priority') + ': ' + ga.getAggregate('COUNT', 'priority'));
}
Scripts - Background is your REPL (Read-Eval-Print Loop) for ServiceNow. Any code you would put in a Business Rule can be prototyped and validated here first — saving significant debugging time.
Note: Scripts - Background runs scripts in the global scope by default. To test a scoped application Script Include, you may need to switch the application context at the top of the page.
The Script Debugger — breakpoints and step-through
The Script Debugger is ServiceNow's interactive debugger — the closest equivalent to a real IDE debugger. It lets you set breakpoints, step through code line by line, and inspect variable values.
Access it at System Diagnostics > Script Debugger.
How to use the Script Debugger
- Open the Script Debugger in a browser tab
- Find your script in the list (Business Rule, Script Include, etc.) and click it to open
- Click in the gutter next to a line number to set a breakpoint
- In another tab, trigger the script (save a record, navigate to a page, etc.)
- The debugger pauses execution at the breakpoint and shows you the current state
- Use the step controls to step over, step into, or step out of functions
- Inspect variables in the Variables panel on the right
The Script Debugger is particularly useful for:
- Business Rules with complex conditional logic where you cannot tell which path executed
- Script Includes with multiple method calls where intermediate values matter
- Debugging recursion or unexpected loops
- Verifying that the GlideRecord object contains what you expect at a specific point
Try/catch — structured error handling
Good scripts do not crash silently. Use try/catch to handle errors gracefully and log useful information:
// Basic try/catch
try {
var gr = new GlideRecord('incident');
gr.get(sys_id);
var result = processIncident(gr);
return result;
} catch (e) {
gs.error('processIncident failed: ' + e.message + ' | Stack: ' + e.stack, 'IncidentProcessor');
return null;
}
// Try/catch with specific error types
try {
var data = JSON.parse(jsonString);
return processData(data);
} catch (e) {
if (e instanceof SyntaxError) {
gs.error('Invalid JSON received: ' + jsonString.substring(0, 100), 'DataProcessor');
} else {
gs.error('Unexpected error: ' + e.message, 'DataProcessor');
}
return null;
}
Best practices for try/catch:
- Log the error message AND the stack trace — the stack trace shows you exactly which line failed
- Log context — include the sys_id, the record number, or whatever value was being processed when the error occurred
- Return a safe default from catch blocks — never let the catch block re-throw unless you have a specific reason
- Do not catch errors you cannot handle — a broad try/catch around your entire script hides bugs; use targeted try/catch around specific risky operations
Checking if a GlideRecord returned results
One of the most common script bugs is failing to check whether a GlideRecord query returned any results before accessing values:
// Wrong — gr.next() returns false if no records found, getValue returns null
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC9999999');
gr.query();
gs.log(gr.getValue('state')); // Logs null — no error, silent failure
// Right — always check gr.next() returned true
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC9999999');
gr.query();
if (!gr.next()) {
gs.warn('Incident INC9999999 not found', 'MyScript');
return;
}
gs.log('State: ' + gr.getValue('state'));
// Using gr.get() — returns true if found, false if not
var gr = new GlideRecord('incident');
if (!gr.get('INC_SYS_ID')) {
gs.error('Record not found: INC_SYS_ID', 'MyScript');
return null;
}
// Safe to access fields now
var state = gr.getValue('state');
Debugging Business Rules — the most common scenarios
Business Rule is not firing
If a Business Rule appears to not be running:
- Check the Active checkbox — the most common cause
- Check the When setting — Before, After, Async, or Display — make sure it matches when you expect it to run
- Check the Condition — if the Business Rule has a condition, verify it is actually evaluating to true for your test record
- Check the Insert/Update/Delete/Query checkboxes — is the relevant action checked?
- Add gs.log('BR is running', 'MyBR') as the first line and check the system log
Business Rule is running but not doing what I expect
(function executeRule(current, previous) {
// Log the starting state to verify what values are present
gs.log('BR starting | state: ' + current.getValue('state') +
' | prev state: ' + previous.getValue('state'), 'MyBR');
// Log before and after key operations
gs.log('Before setValue | priority: ' + current.getValue('priority'), 'MyBR');
current.setValue('priority', '1');
gs.log('After setValue | priority: ' + current.getValue('priority'), 'MyBR');
})(current, previous);
current vs previous in Business Rules
A common source of confusion: current is the record being saved with its new values; previous is the record as it was before the save. In an Insert rule, previous has no values (it did not exist yet). In an Update rule, previous has the old values and current has the new ones.
// Check if a specific field changed in an Update rule
if (current.state != previous.state) {
gs.log('State changed from ' + previous.getValue('state') +
' to ' + current.getValue('state'), 'StateChangeLog');
}
// Check if this is an insert vs update
if (current.operation() == 'insert') {
gs.log('New record created', 'MyBR');
} else if (current.operation() == 'update') {
gs.log('Existing record updated', 'MyBR');
}
For a full breakdown of Business Rule types and when each fires, see the complete guide to Business Rule types.
Debugging Client Scripts
Client Scripts run in the browser. Open Chrome DevTools (F12 or right-click > Inspect) and go to the Console tab. You can use console.log() inside Client Scripts:
function onChange(control, oldValue, newValue, isLoading) {
console.log('onChange fired | isLoading: ' + isLoading + ' | newValue: ' + newValue);
if (isLoading) return;
console.log('g_form state value: ' + g_form.getValue('state'));
// your logic...
}
The browser console also shows JavaScript errors from Client Scripts. An error in a Client Script typically appears as a red error message in the console with a line number — click it to open the Sources panel and see the error in context.
To find which Client Scripts are loading on a form: open the browser console and type document.getElementById('client_script_list') or look for Client Script execution messages if your instance has diagnostics enabled.
Common errors and what they mean
"TypeError: Cannot read property 'xxx' of null"
Almost always means you are trying to access a field or call a method on a null or undefined value — typically a GlideRecord query that returned no results, a getParameter() that returned null, or a JSON parse that produced null.
// Fix: check for null before accessing
var gr = new GlideRecord('sys_user');
gr.get(userId);
if (!gr || !gr.isValid()) {
gs.warn('User not found: ' + userId, 'MyScript');
return;
}
var dept = gr.getValue('department'); // Safe now
"ReferenceError: MyScriptInclude is not defined"
The Script Include name in your script does not match the Script Include record name, or the Script Include is in a different scope and is not accessible. Check the Script Include record name and the "Accessible from" field.
"Security restricted: Table 'table_name'"
Your script is trying to access a table that the current user's ACLs do not allow. This can happen in Business Rules that run in user context rather than admin context. Use gs.hasRole('admin') to check permissions, or run the query with elevated privileges where appropriate.
Script fires but changes are not saved
In a Before Business Rule, you can set values on current and they will be saved automatically — you do not call current.update(). Calling current.update() in a Before rule causes a redundant database write. In an After rule, you must call current.update() to persist changes.
Debugging Scheduled Jobs
Scheduled Jobs run in the background and do not have a trigger you can watch. To debug them:
- Set the Next action date to run in the next minute and watch the system log
- Or click "Execute Now" on the Scheduled Job record
- Filter the system log by the job name (set as the gs.log source)
- Check System > System Log > Errors for any exceptions thrown by the job
For more on Scheduled Jobs, see the guide to Scheduled Script Executions.
The debugging workflow — practical approach
When a script is not working as expected, follow this sequence:
- Reproduce in Scripts - Background first — pull out the core logic, run it manually, and see what happens. Most bugs are immediately obvious when you can run the code interactively.
- Add gs.log() statements at key points — log inputs, intermediate values, and the path taken through conditional logic.
- Check the system log for errors — filter by your source name and look for Error or Warning entries.
- Use the Script Debugger for complex flow issues — when you have multiple code paths and cannot tell which one executed.
- Check the record state in the UI — verify that the record you triggered the script on actually has the values you think it has.
Related guides that complete the debugging picture:
- Script Includes guide — how to test Script Include methods in isolation
- Client Script types — client-side debugging with browser developer tools
- Business Rule types — understanding when each rule fires
- GlideRecord performance tips — diagnosing slow query issues