SSJS has several unique failure modes that differ from standard JavaScript. Defensive coding means proactively guarding against these platform-specific behaviors.

1. ParseJSON Argument Guard

Contrary to a widespread belief, Platform.Function.ParseJSON(null) and ParseJSON(undefined) do not error — they return null (runtime-verified, see Known Bugs). What actually throws is a wrong argument count and a non-string object or array argument. Coercing with + "" keeps such a value a string, so it is still the safe habit — but you must also check the result for null:

// RISKY — throws if rawBody is an object or array
var data = Platform.Function.ParseJSON(rawBody);

// CORRECT — coerce to a string, then check for null
var data = Platform.Function.ParseJSON(rawBody + "");
if (!data) {
    Write("No data or invalid JSON");
    return;
}

// Also safe for HTTP response content (CLR string)
var resp = req.send();
var data = Platform.Function.ParseJSON(String(resp.content) + "");

Show test script
<script runat="server">
/*
 * Chapter: ParseJSON Argument Guard
 * Proves:
 *   1. ParseJSON(null) returns null (does not throw).
 *   2. ParseJSON({}) throws.
 *   3. + '' coercion path works for strings.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var n = Platform.Function.ParseJSON(null);
assert("ParseJSON(null) is null", n === null ? "null" : "other", "null");
assertThrows("ParseJSON({}) throws", function () { return Platform.Function.ParseJSON({}); });
var data = Platform.Function.ParseJSON('{"a":1}' + "");
assert("coerced string parse", data.a, 1);
</script>

2. Lookup Returns null — and a CLR null for an Empty Field

Platform.Function.Lookup returns a genuine JS null when no matching row exists (runtime-verified). The trap is a different case: a row that exists but whose field was never populated returns a CLR null, which is not === null and throws the moment you coerce it — so if (!email) is not a safe guard.

var email = Platform.Function.Lookup("Contacts", "Email", "Id", contactId);

// WRONG — throws "Object cannot be cast from DBNull to other types." on a CLR null
if (!email) { /* ... */ }

// CORRECT — coerce with String() first, then test the string
var value = String(email);   // "null" when no row matched, "" when the field is empty
if (value === "" || value === "null") {
    Write("Contact not found or no email on file");
    return;
}

See Lookup for all four empty-ish outcomes.


Show test script
<script runat="server">
/*
 * Chapter: Lookup Returns null
 * Proves:
 *   1. Lookup on missing DE/row yields String(...) usable guard path.
 *   NON-ASSERTABLE here: CLR-null empty-field without a fixture row.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var email = null;
var value = String(email);
assert("String(null) guard for Lookup miss", value, "null");
assert("Lookup typeof", typeof Platform.Function.Lookup, "clrmethodinfo");
</script>

3. GetPostData() One-Time Read

Platform.Request.GetPostData() returns "" on the second call. Read once, save to variable.

// WRONG
function getField(name) {
    var body = Platform.Request.GetPostData(); // second call returns ""
    return Platform.Function.ParseJSON(body + "")[name];
}

// CORRECT — read once at top of script
var rawBody = Platform.Request.GetPostData();
var body = Platform.Function.ParseJSON(rawBody + "");

function getField(name) {
    return body[name];
}

Show test script
<script runat="server">
/*
 * Chapter: GetPostData One-Time Read
 * Proves:
 *   1. GetPostData callable; second call on GET is empty string.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var a = Platform.Request.GetPostData();
var b = Platform.Request.GetPostData();
assert("second GetPostData empty on GET", b === "" || b === null ? "empty" : "other", "empty");
assert("first typeof string-or-empty", typeof a === "string" || a === null ? "ok" : typeof a, "ok");
</script>

4. Type Coercion in Comparisons

Platform.Function.Lookup hands back each column’s native runtime type — a Number/Decimal column arrives as a number, a Boolean column as a boolean, Text as a string, and a Date column as a real Date. DataExtension.Rows.Retrieve() does the opposite and stringifies every field. Know which of the two you called before you compare:

// Lookup — a Number column is already a number, so a numeric compare is correct
var score = Platform.Function.Lookup("Scores", "value", "userId", userId);
if (score > 80) { /* ... */ }

// Rows.Retrieve — every field arrives as a string, so convert before comparing
Platform.Load("core", "1.1.5");
var row = DataExtension.Init("Scores").Rows.Retrieve({
    Property: "userId", SimpleOperator: "equals", Value: userId
})[0];
if (parseInt(row.value, 10) > 80) { /* ... */ }

// Booleans: Lookup yields a real boolean, Rows.Retrieve the capitalized text "True"/"False"
var isActive = Platform.Function.Lookup("Contacts", "active", "id", id);
if (isActive === true) { /* ... */ }
if (row.active === "True") { /* ... */ }

Show test script
<script runat="server">
/*
 * Chapter: Type Coercion in Comparisons
 * Proves:
 *   1. parseInt clean string; boolean True string compare pattern.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("parseInt clean", parseInt("81", 10) > 80 ? "true" : "false", "true");
assert("True string compare", "True" === "True" ? "true" : "false", "true");
Platform.Load("core", "1.1.5");
assert("DataExtension after Load", typeOfThunk(function () { return typeof DataExtension; }), "object");
</script>

5. Property Access on Possibly-Null Objects

Always check for existence before accessing nested properties:

var result = Platform.Function.LookupRows("DE", "Status", "active");

// WRONG — LookupRows returns null (not an empty array) when nothing matches,
//         so result[0] is a TypeError on null
var firstEmail = result[0].Email;

// CORRECT
if (result && result.length > 0) {
    var firstEmail = result[0].Email;
}

// For WSProxy results
var wsResult = proxy.retrieve("DataExtension", ["Name", "CustomerKey"]);
if (wsResult.Status === "OK" && wsResult.Results && wsResult.Results.length > 0) {
    var de = wsResult.Results[0];
}

Show test script
<script runat="server">
/*
 * Chapter: Property Access on Possibly-Null Objects
 * Proves:
 *   1. null guard before [0] access.
 *   2. LookupRows no-match is null.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var result = null;
var safe = (result && result.length > 0) ? result[0].Email : "";
assert("null guard", safe, "");
assert("LookupRows typeof", typeof Platform.Function.LookupRows, "clrmethodinfo");
// LookupRows returns null on no match when the DE exists — simulated here
var rows = null;
assert("no-match modeled as null", rows === null ? "null" : "other", "null");
</script>

6. Undefined vs Missing Properties

SSJS inherits JavaScript’s behavior where accessing a missing property returns undefined, not null:

var obj = { name: "Jane" };
var email = obj.email;    // undefined
var name = obj.name;      // "Jane"

// Guard for undefined
if (typeof obj.email !== "undefined") {
    sendEmail(obj.email);
}

// Provide a default
var email = obj.email || "no-reply@example.com";

Show test script
<script runat="server">
/*
 * Chapter: Undefined vs Missing Properties
 * Proves:
 *   1. missing prop is undefined; hasOwnProperty false.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var o = { a: 1 };
assert("missing undefined", o.b === undefined ? "undefined" : "other", "undefined");
assert("hasOwn false", o.hasOwnProperty("b") ? "true" : "false", "false");
assert("hasOwn true", o.hasOwnProperty("a") ? "true" : "false", "true");
</script>

7. For…in Without hasOwnProperty

for...in iterates prototype properties. Always check hasOwnProperty:

var config = Platform.Function.ParseJSON(configJson + "");

// WRONG — may iterate inherited prototype methods
for (var key in config) {
    Write(key + ": " + config[key]);
}

// CORRECT
for (var key in config) {
    if (config.hasOwnProperty(key)) {
        Write(key + ": " + config[key]);
    }
}

Show test script
<script runat="server">
/*
 * Chapter: For...in Without hasOwnProperty
 * Proves:
 *   1. hasOwnProperty filters own keys.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var config = { x: 1, y: 2 }, n = 0;
for (var k in config) { if (config.hasOwnProperty(k)) n++; }
assert("own key count", n, 2);
</script>

8. Switch Default Bug Workaround

The switch statement’s default case may not execute in SSJS. Use if/else if for critical control flow or add an explicit fallback:

// Potentially buggy in SSJS
switch(status) {
    case "active":
        handleActive();
        break;
    default:
        handleUnknown(); // may not execute!
}

// Safer
if (status === "active") {
    handleActive();
} else if (status === "inactive") {
    handleInactive();
} else {
    handleUnknown();
}

Show test script
<script runat="server">
/*
 * Chapter: Switch Default Bug Workaround
 * Proves:
 *   1. Pre-check map avoids relying on default.
 *   2. DEV empty-case fallthrough.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var status = "unknown";
var validStatuses = { active: true, inactive: true };
var out = "";
if (!validStatuses[status]) { out = "Unknown status"; }
else {
    switch (status) {
        case "active": out = "Active"; break;
        case "inactive": out = "Inactive"; break;
    }
}
assert("pre-check unknown", out, "Unknown status");
var level = "admin", access = "";
switch (level) { case "admin": case "superuser": access = "Full access"; break; }
assert("DEV empty fallthrough", access === "" ? "empty" : access, "empty");
</script>

9. Empty Checks

Use a consistent utility function for all emptiness checks:

function isEmpty(val) {
    return val === null
        || typeof val === "undefined"
        || val === ""
        || (typeof val === "string" && val.replace(/\s/g, "") === "");
}

isEmpty(null);       // true
isEmpty("");         // true
isEmpty("  ");       // true
isEmpty("hello");    // false
isEmpty(0);          // false

Show test script
<script runat="server">
/*
 * Chapter: Empty Checks
 * Proves:
 *   1. !"" and == null patterns.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("empty string falsy", !"" ? "true" : "false", "true");
assert("null == null", null == null ? "true" : "false", "true");
var s = "x";
assert("non-empty truthy", s ? "true" : "false", "true");
</script>

10. Division and Modulo Safety

Guard against division by zero:

var total = rows.length;

// WRONG
var average = sumValue / total;  // NaN if total === 0

// CORRECT
var average = total > 0 ? sumValue / total : 0;
Show test script
<script runat="server">
/*
 * Chapter: Division and Modulo Safety
 * Proves:
 *   1. Guard divisor zero; modulo works.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
function safeDiv(a, b) { if (!b) return null; return a / b; }
assert("div by zero null", safeDiv(1, 0) === null ? "null" : "other", "null");
assert("div ok", safeDiv(10, 2), 5);
assert("mod", 10 % 3, 1);
</script>

See Also