Defensive Coding
Guard against null values, empty strings, type coercion bugs, and unexpected DE return values with defensive SSJS patterns.
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>