July 16, 2026 · 7 min read
JavaScriptServiceNowEquality in JavaScript — and Where It Bites in ServiceNow Development
ServiceNow development is full of comparisons. Is this field's value '1'? Is the state closed? Did the user pick the same assignment group as before? Is this the same object I stored a moment ago, or a fresh copy from the server?
Every one of those questions goes through JavaScript equality — and because we write JavaScript on every surface of the platform (Rhino in business rules and script includes, the browser in client scripts, widget controllers, and UX framework components), the way equality behaves shapes code on all of them. This is how I understand it after years of working with it across the platform.
The three kinds of equality
JavaScript gives you three ways to ask "are these the same?":
- Strict equality —
a === b. What you should reach for by default. - Loose equality —
a == b. Converts types before comparing. The source of most "works on my instance" surprises. - Same-value equality —
Object.is(a, b). Rarely written, but it's the cleanest definition of "the same value," so it's the best place to start.
How I understand "the same value": Object.is
Object.is(a, b) answers one question: do a and b point at the same value?
For primitives, that matches intuition:
Object.is('INC0010001'.toLowerCase(), 'inc0010001'); // true — same string value
Object.is(2, 2); // true
For objects, "same value" means same object — not "looks identical":
Object.is({}, {}); // false — two different objects that happen to be empty
The way I keep this straight: variables don't hold objects, they point at them. Assignment points a new name at an existing object — it never copies. This matters constantly in Service Portal widget controllers:
// Widget client controller
api.controller = function ($scope) {
var c = this;
var draft = c.data.request; // NOT a copy — another name for the same object
draft.short_description = 'Updated';
console.log(c.data.request.short_description); // 'Updated' — you changed "both"
};
Object.is(draft, c.data.request) is true — two names, one object. Mutate through either name and the change is visible through both. Half of the "my widget's data changed by itself" mysteries I've debugged were exactly this: an accidental alias, not a copy.
The flip side bites too. After a c.server.update() round trip, the objects inside data are rebuilt from the server's JSON response. The new c.data.request may be field-for-field identical to the old one, but it is a different object — any reference you saved before the round trip now points at stale data. Identical-looking is not the same as same.
Strict equality: === and its two weird exceptions
In day-to-day code, a === b behaves exactly like Object.is(a, b):
current.getValue('state') === '7'; // true when state is 7 — getValue returns strings
{} === {}; // false — different objects
There are exactly two values where they disagree, and both are numbers.
Exception 1: NaN === NaN is false. NaN is the one value in JavaScript that is not strict-equal to itself. On the platform you usually meet it through parsing:
// Client script
var estimate = parseInt(g_form.getValue('u_estimated_hours'), 10);
// Field empty? estimate is NaN.
if (estimate === NaN) {
// Dead code — this comparison is ALWAYS false
}
if (Number.isNaN(estimate)) {
// This is the correct check
}
Number.isNaN(x) is the fix. (Object.is(x, NaN) also works, and the old idiom x !== x is true only for NaN — a fun interview question, but write Number.isNaN in real code.)
One platform caveat: classic Rhino server scripts run in ES5 mode, where Number.isNaN and Object.is don't exist. There, x !== x still detects NaN reliably. In the browser — and in scoped apps running the ES2021 engine — you have the full toolkit.
Exception 2: 0 === -0 is true, even though floating-point math treats them as two distinct values (Object.is(0, -0) is false). In ten-plus years of platform work I have never once had -0 matter in a business rule or client script. Know it exists, then move on.
Loose equality: why legacy ServiceNow code is full of ==
Here's the platform detail that explains a lot of ServiceNow code: g_form.getValue() always returns a string — priority 1 comes back as '1'. So:
g_form.getValue('priority') === 1; // false — string vs number, no conversion
g_form.getValue('priority') == 1; // true — == converts '1' to 1 first
g_form.getValue('priority') === '1'; // true — the honest version
This is exactly why so much ServiceNow code leans on ==: it papers over the string-vs-number mismatch that the platform APIs create. Server-side Rhino makes it even more seductive, because a GlideRecord field isn't a string either — it's a GlideElement object:
// Business rule / script include
var gr = new GlideRecord('incident');
gr.get(sysId);
gr.state == '7'; // true — GlideElement coerces to its string value
gr.state === '7'; // false! An object is never === a string
gr.getValue('state') === '7'; // true — and refactor-safe
gr.state == '7' works, and that's the problem: it trains you to trust ==, right up until you compare two fields directly — gr.state == gr2.state compares two GlideElement objects, not their string values — or carry the habit into client-side code, where == quietly hides string-vs-number mismatches you'd rather catch. The habit that stays safe on every surface of the platform is the same one: call getValue(), compare with ===.
The conversion rules behind == are genuinely arcane — [] == '' and false == [0] are both true — which is why most style guides tell you to avoid it. The one idiom worth keeping is the null-or-undefined check:
if (returnedValue == null) {
// true for both null and undefined — equivalent to
// (returnedValue === null || returnedValue === undefined)
}
Handy when a script include might return either. Just agree with your team that this is the only sanctioned use of ==.
Where identity really earns its keep: the UX framework
In Next Experience / UI Builder, client state is designed around immutability — the framework decides "did this change?" by comparing object identity, not by deep-inspecting contents. The same applies inside custom components built with the Next Experience UI Framework: you update state through updateState(), never by assignment, because re-renders are triggered by reference changes. Mutating a state object in place means the reference didn't change, so as far as change detection is concerned, nothing happened:
// Custom component action handler — mutation the framework can't see:
state.filters.active = true;
// Replacement it can see — a NEW object, so identity changes:
updateState({ filters: { ...state.filters, active: true } });
Once you think of equality as "same object or not?", this pattern stops being a framework rule you memorize and becomes obvious: the framework is literally asking the same-value question, and a mutated object answers "same as before."
The cheat sheet
g_form.getValue()(client scripts) andgetValue()on GlideRecord (server) return strings. Compare against string literals with===.- Never compare a raw GlideRecord field (
gr.state) with===— it's a GlideElement object. Usegr.getValue('state'). x === NaNis always false. Check withNumber.isNaN(x)in the browser and ES2021 scoped apps; in classic ES5 server scripts,x !== xdoes the job.- Two objects are equal only if they're the same object. Assignments alias; they don't copy. JSON round trips (like
c.server.update()in widgets) create new objects. - In UI Builder and Next Experience UI Framework components, replace state objects instead of mutating them — change detection is an identity check.
- Reserve
==for one job, if any:x == nullas a combined null/undefined check.
Equality issues are rarely dramatic. Nothing throws; a condition just quietly evaluates the way you didn't expect. Once you're clear on same value vs. looks the same — plus the habit of getValue() + === — a whole category of head-scratchers disappears at once.