ServiceNow Server-Side vs Client-Side JavaScript Truthiness for Arrays and Objects Across ES12 Mode and Legacy Modes

Why ES12 changes the game

The greatest danger in times of turbulence is not the turbulence; it is to act with yesterday's logic.
– Peter Drucker

Executive summary

Modern browsers (client-side) treat arrays/objects as truthy because they are objects and therefore always coerce to true in boolean contexts (if (...), &&, ||). This follows the ECMAScript specification’s ToBoolean operation.[1] MDN’s reference examples explicitly show if([]) is truthy and that “all objects become true.”[2][3]

On the ServiceNow platform, server-side JavaScript historically ran under older engine/mode combinations. Community reports show a corner case where an empty array [] is treated as falsy in some server-side contexts, even though it is truthy in browsers and per spec.[6] With the platform’s ECMAScript 2021 (ES12) mode enabled for scripts, ServiceNow warns that behavior may change when switching JavaScript modes—because the engine and language features differ.[4]

Takeaway: In production integrations, never use if (value) to mean “has elements” for arrays or “has keys” for objects. Always use explicit checks (Array.isArray, .length, Object.keys) and return explicit status flags (ok, status, error) across server/client boundaries.

Your observation, explained

Your test is a perfect example of why “truthiness” should never be used as a proxy for “non-empty.” In standard JavaScript, this is always true:

// Standard JavaScript behavior (browsers, and spec):
var response = [];
if (response) {
  // Runs, because [] is an object and therefore truthy
}

But in legacy server-side modes, ServiceNow can treat empty arrays as falsy (community-confirmed).[6] When you enable ES12 mode for a script, you often get behavior that matches modern JavaScript more closely, and ServiceNow explicitly warns behavior may change when switching modes.[4][7]

Why this becomes a “weeks of debugging” integration bug

Integration bugs get nasty when:

  • Different runtimes disagree: browser client scripts follow spec; older server scripts might not for this edge case.
  • Branching happens on the wrong signal: an empty-but-valid result set becomes “failure.”
  • Retries and idempotency collide: you retry “failures” that were actually “empty success,” causing duplicates or inconsistent states.
  • Logging misleads: logs show “failed” even though HTTP 200 and a valid JSON payload were returned.
  • GlideAjax adds async timing: bad truthiness checks plus async callbacks often hide the real root cause.

ServiceNow’s GlideAjax API is explicitly asynchronous by default (getXML() / getXMLAnswer()), and synchronous calls (getXMLWait()) are discouraged because they can freeze the UI and have availability limitations (for example, scoped application restrictions).[5] That means you need stable, explicit “contract fields” in your payload (like ok, error, count) instead of relying on JavaScript truthiness.

The golden rule: treat “missing”, “empty”, and “invalid” as three different states

Anti-pattern (bug magnet):

// BAD: ambiguous. What does "truthy" mean here?
if (items) {
  process(items);
} else {
  throw new Error("Integration failed");
}

Production-grade pattern (explicit):

function isNonEmptyArray(x) {
  return Array.isArray(x) && x.length > 0;
}
function isPlainObject(x) {
  return x !== null && typeof x === "object" && !Array.isArray(x);
}
function hasKeys(obj) {
  return isPlainObject(obj) && Object.keys(obj).length > 0;
}

// Now your intent is explicit:
if (isNonEmptyArray(items)) {
  process(items);
} else if (Array.isArray(items)) {
  // empty-but-valid
  processEmptyResult();
} else {
  // missing or invalid
  throw new Error("Invalid response shape");
}

Four real-world shapes that routinely break integrations

Below are common response shapes from REST/SOAP gateways, MID scripts, data brokers, or Script Includes. Each example shows a bad version (truthiness trap) and a good version (explicit checks).

1) Array of arrays

Example: a batch integration returns multiple pages, each page is an array of records.

// Example response:
var pages = [
  [ {id:"A1"}, {id:"A2"} ],
  [] // page 2 is empty (valid)
];

// BAD:
if (pages[1]) {                 // might be treated falsy server-side in legacy modes
  gs.info("Page 2 has data");
} else {
  gs.info("Page 2 failed");     // wrong: empty page isn't failure
}

// GOOD:
if (Array.isArray(pages[1]) && pages[1].length === 0) {
  gs.info("Page 2 is empty (valid). Stop paging.");
} else if (Array.isArray(pages[1])) {
  gs.info("Page 2 has " + pages[1].length + " rows.");
} else {
  gs.error("Page 2 missing/invalid");
}

2) Array of objects

Example: external API returns a list of incidents to sync.

// Example response:
var incidents = []; // empty means "nothing to sync", not "failure"

// BAD:
if (incidents) {
  sync(incidents);
} else {
  // In legacy server-side mode, this branch might run:
  throw new Error("Sync failed");
}

// GOOD:
if (!Array.isArray(incidents)) throw new Error("Invalid incidents array");
if (incidents.length === 0) {
  gs.info("No incidents to sync (valid).");
} else {
  sync(incidents);
}

3) Object of arrays

Example: a gateway returns categorized results by type.

// Example response:
var payload = {
  users: [],
  groups: ["G1","G2"]
};

// BAD:
if (payload.users) { // empty array might look falsy server-side (legacy)
  gs.info("Users present");
} else {
  gs.info("User fetch failed"); // wrong
}

// GOOD:
if (!payload || typeof payload !== "object") throw new Error("Bad payload");
if (!Array.isArray(payload.users)) throw new Error("payload.users must be an array");
if (payload.users.length === 0) {
  gs.info("Users array is empty (valid).");
}

4) Object of objects

Example: integration response includes nested status blocks and metadata.

// Example response:
var resp = {
  ok: true,
  data: {
    account: { id: "ACCT-9", region: "IN" },
    metrics: {} // empty metrics is valid, means "not provided"
  }
};

// BAD:
if (resp.data.metrics) { // {} is always truthy in spec, but don't rely on truthiness anyway
  useMetrics(resp.data.metrics);
}

// GOOD:
function hasKeys(obj) {
  return obj && typeof obj === "object" && !Array.isArray(obj) && Object.keys(obj).length > 0;
}
if (hasKeys(resp.data.metrics)) {
  useMetrics(resp.data.metrics);
} else {
  gs.info("No metrics provided (valid).");
}

GlideAjax + client-callable Script Include: a robust “contract-first” pattern

The safest way to cross the client/server boundary is to return a single JSON string that always contains: ok, data, error, and meta. Then the client checks ok === true, not truthiness of data. The platform’s GlideAjax documentation describes the calling pattern and the recommended asynchronous methods.[5][8]

Server side (Script Include: Client callable)

var IntegrationAjax = Class.create();
IntegrationAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

  // Client calls sysparm_name=getOrders
  getOrders: function () {
    var customerId = this.getParameter("sysparm_customer_id");

    var out = {
      ok: true,
      data: [],
      error: null,
      meta: { customerId: customerId, count: 0 }
    };

    try {
      if (!customerId) throw new Error("Missing sysparm_customer_id");

      // --- Example: pretend we called an external API and got an array back ---
      // IMPORTANT: empty array means "0 orders", NOT "failed"
      var orders = []; // could be empty

      if (!Array.isArray(orders)) throw new Error("orders must be an array");

      out.data = orders;
      out.meta.count = orders.length;

      return JSON.stringify(out);

    } catch (e) {
      out.ok = false;
      out.error = String(e);
      out.data = [];
      out.meta.count = 0;
      return JSON.stringify(out);
    }
  },

  type: "IntegrationAjax"
});

Client side (Client Script)


function fetchOrders(customerId) {
  var ga = new GlideAjax("IntegrationAjax");
  ga.addParam("sysparm_name", "getOrders");
  ga.addParam("sysparm_customer_id", customerId);

  ga.getXMLAnswer(function (answer) {
    var payload;
    try {
      payload = JSON.parse(answer);
    } catch (e) {
      console.error("Bad JSON from server", e, answer);
      return;
    }

    if (payload.ok !== true) {
      console.error("Server reported error:", payload.error);
      return;
    }

    // Now data handling is explicit and stable across modes:
    if (!Array.isArray(payload.data)) {
      console.error("Invalid data shape");
      return;
    }

    if (payload.data.length === 0) {
      console.log("No orders (valid).");
      // show empty state in UI
      return;
    }

    console.log("Orders:", payload.data);
    // render list
  });
}

This pattern survives ES12 vs legacy differences because it never uses if(payload.data) to decide success.

Industry-standard best practices for avoiding truthiness traps

  1. Never treat an array/object as a boolean signal. Use Array.isArray(x) && x.length > 0 and Object.keys(obj).length > 0.
  2. Define an explicit “integration contract”. Always return ok, error, and meta.count (or similar) across Scripted REST, Script Includes, and GlideAjax.
  3. Log the shape, not the truthiness. Example: gs.info("orders: isArray=" + Array.isArray(orders) + ", count=" + (orders ? orders.length : "n/a")).
  4. Be cautious when switching JavaScript modes. ServiceNow explicitly notes switching to ES12 can change behavior—plan regression tests for boolean contexts and coercions.[4]
  5. Prefer async GlideAjax. ServiceNow recommends asynchronous calls (getXML()/getXMLAnswer()) and warns synchronous calls can degrade UX and have limitations.[5]

Industry-standard best practices for integration-safe JavaScript in ServiceNow

  1. Never use bare truthiness for arrays/objects in integration logic. Use explicit checks: Array.isArray(x), x.length, Object.keys(x).length.
  2. Normalize payloads at boundaries (REST response parsing, Script Include return values, GlideAjax). Always return the same top-level schema: e.g. { ok, data, errors, meta }.
  3. Make “no data” a first-class outcome. Decide explicitly what empty arrays/objects mean (not found vs success with no work vs non-retriable error).
  4. Log structure, not just messages. Log the shape of payloads (types, keys, lengths) rather than printing entire sensitive bodies.
  5. Use idempotency keys for writes. If truthiness bugs cause retries, idempotency prevents duplicates.
  6. Align runtime settings across environments. Ensure DEV/TEST/UAT/PRE-PROD/PROD have consistent scripting mode settings where possible, and document exceptions.
  7. Write tests that include empty arrays/objects. Your integration “happy path” must include responses like [], {}, missing keys, and nulls.

Takeaway

The fastest way to lose days in integration debugging is to treat if (value) as “value has meaningful data”. Arrays and objects need structure checks (type + length/keys), not truthiness checks.

If enabling ES12 mode changes behavior in your instance, treat that as a signal that some parts of your code were depending on non-standard coercions. Fix those areas by making intent explicit and by normalizing payloads at boundaries (REST/GlideAjax/Script Includes).

Summary:

Stop using “truthy/falsy” as business logic for arrays/objects; treat “missing”, “empty”, and “invalid” as distinct states, return explicit status flags, and use explicit shape checks—especially when your code crosses ServiceNow server/client boundaries and when ES12 mode may change legacy behavior.[4]

References and acknowledgements (numbered footnotes)

  1. Ecma International, ECMAScript® 2021 Language SpecificationToBoolean (objects are truthy). https://262.ecma-international.org/12.0/#sec-toboolean
  2. Mozilla (MDN Web Docs Contributors), “Truthy” (examples include if([]) and if({})). https://developer.mozilla.org/en-US/docs/Glossary/Truthy
  3. Mozilla (MDN Web Docs Contributors), “Boolean” (summary: “All objects become true” in boolean coercion). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
  4. ServiceNow Docs, “Turn on ECMAScript 2021 (ES12) mode for a script” (includes note that switching modes can change script behavior). https://www.servicenow.com/docs/bundle/xanadu-application-development/page/script/JavaScript-engine-upgrade/concept/set-es12-mode-scripts.html
  5. ServiceNow Docs (Zurich API Reference), “GlideAjax — Client” (async getXML()/getXMLAnswer(); cautions and limitations for getXMLWait()). https://www.servicenow.com/docs/en-US/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideAjax/concept/c_GlideAjaxAPI.html
  6. ServiceNow Community (Andy-L), “Falsy and Truthy and empty arrays in ServiceNow Javascript” (reports empty arrays behaving as falsy server-side). https://www.servicenow.com/community/developer-forum/falsy-and-truthy-and-empty-arrays-in-servicenow-javascript/m-p/3353036
  7. ServiceNow Community (Jan S.), “ECMAScript 2021 (ES12) mode in individual script…” (practical notes on the ES12 toggle storage/behavior). https://www.servicenow.com/community/servicenow-ai-platform-articles/ecmascript-2021-es12-mode-in-individual-script-there-s-an/ta-p/3026751
  8. ServiceNow Community, “GlideAjax Troubleshooting Guide” (security notes on client-callable Script Includes; public vs private method naming). https://www.servicenow.com/community/in-other-news/glideajax-troubleshooting-guide/ba-p/2267316

Acknowledgement: The JavaScript truthiness definitions are grounded in the ECMAScript specification and MDN documentation. The ServiceNow ES12 and GlideAjax behavior is documented in ServiceNow product documentation and further discussed in ServiceNow Community threads.