Control Flow
if/else, ternary, switch (and its SFMC bug), and branching patterns in SSJS.
if / else
var score = 85;
if (score >= 90) {
Write("A");
} else if (score >= 80) {
Write("B");
} else if (score >= 70) {
Write("C");
} else {
Write("F");
}
Omitting braces works for single statements but is not recommended:
if (valid) doSomething(); // works but fragile
Truthy / Falsy Checks
var value = Platform.Request.GetQueryStringParameter("id");
// Preferred: !! coercion or direct truthiness check
if (!value) {
// value is "", null, undefined, 0, or false
Platform.Response.Redirect("/error", false);
}
// Explicit empty string check
if (value === "" || value === null) {
// strictly no value
}
SFMC functions often return "" (empty string) rather than null/undefined when data is missing. Use !value or value === "" accordingly.
Boolean Coercion Patterns
// Convert to boolean explicitly
var hasValue = !!value;
var isActive = !!subscriber.active;
// Short-circuit for defaults
var name = Platform.Request.GetQueryStringParameter("name") || "Subscriber";
// Safe property access (no optional chaining in SSJS)
var city = (person && person.address && person.address.city) || "Unknown";
Show test script
<script runat="server">
/*
* Chapter: if / else
* Proves:
* 1. if/else-if/else.
* 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");
}
var grade = "B", out = "";
if (grade === "A") out = "A"; else if (grade === "B") out = "B"; else out = "F";
assert("elseif", out, "B");
</script>
switch
The switch statement works but has a known SFMC engine bug:
The
defaultcase may not execute reliably. If none of thecasevalues match and execution reachesdefault, the engine may skip it.
var status = "pending";
switch (status) {
case "active":
Write("Active");
break;
case "inactive":
Write("Inactive");
break;
default:
// ⚠️ This may NOT execute in SSJS
Write("Unknown status");
}
Safe pattern: Replace default with an explicit final else:
// Safer approach — use if/else for the fallback
var output = "";
if (status === "active") {
output = "Active";
} else if (status === "inactive") {
output = "Inactive";
} else {
output = "Unknown status"; // Always works
}
Write(output);
Or check explicitly before the switch:
var validStatuses = { active: true, inactive: true };
if (!validStatuses[status]) {
Write("Unknown status");
} else {
switch (status) {
case "active": Write("Active"); break;
case "inactive": Write("Inactive"); break;
}
}
Fall-through
Empty stacked case labels do not fall through reliably in SFMC SSJS. Matching the first empty label leaves the shared body unexecuted:
var level = "admin";
var access = "";
switch (level) {
case "admin": // matches here…
case "superuser": // …but the body below does NOT run for "admin"
access = "Full access";
break;
case "user":
access = "Limited access";
break;
}
// access is still "" when level === "admin"
// access is "Full access" when level === "superuser"
Duplicate the body (or use if / a lookup map) instead of relying on empty-case fall-through. See also the related switch break-escape bug.
Show test script
<script runat="server">
/*
* Chapter: switch
* Proves:
* 1. case + default + fall-through.
* 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");
}
var status = "inactive", out = "";
switch (status) { case "active": out = "Active"; break; case "inactive": out = "Inactive"; break; default: out = "Unknown status"; }
assert("inactive", out, "Inactive");
var level = "admin", access = "";
switch (level) { case "admin": case "superuser": access = "Full access"; break; case "user": access = "Limited access"; break; }
assert("DEV empty-case fallthrough skips body for first label (spec: runs)", access === "" ? "empty" : access, "empty");
level = "superuser"; access = "";
switch (level) { case "admin": case "superuser": access = "Full access"; break; case "user": access = "Limited access"; break; }
assert("matching non-empty stacked case runs body", access, "Full access");
</script>
Ternary
The ternary operator condition ? thenValue : elseValue is fully supported:
var greeting = hour < 12 ? "Good morning" : "Good afternoon";
var label = count === 1 ? "item" : "items";
var cssClass = isActive ? "active" : "inactive";
Avoid deeply nested ternaries — they become unreadable quickly.
Show test script
<script runat="server">
/*
* Chapter: Ternary
* Proves:
* 1. Ternary values.
* 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");
}
var hour = 9; assert("morning", hour < 12 ? "Good morning" : "Good afternoon", "Good morning");
assert("items", 1 === 1 ? "item" : "items", "item");
</script>
Short-Circuit Evaluation
Use && and || for concise conditional execution:
// Execute only if condition is true
isDebug && Write("<pre>" + Platform.Function.Stringify(data) + "</pre>");
// Default value
var timeout = config.timeout || 30;
// Safe property access
var name = user && user.profile && user.profile.firstName;
Show test script
<script runat="server">
/*
* Chapter: Short-Circuit
* Proves:
* 1. && || short-circuit.
* 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");
}
var wrote = false; true && (wrote = true); assert("and true", wrote ? "true" : "false", "true");
wrote = false; false && (wrote = true); assert("and false", wrote ? "true" : "false", "false");
var config = {}; assert("or default", config.timeout || 30, 30);
var user = { profile: { firstName: "Ada" } };
assert("safe access", user && user.profile && user.profile.firstName, "Ada");
</script>
Throw / Early Return
Use throw or early returns to guard against invalid states:
function processSubscriber(sk) {
if (!sk) {
throw new Error("SubscriberKey is required");
}
// String() first — a Lookup result throws on a truthiness test when the field is empty
var email = String(Platform.Function.Lookup("Subscribers", "Email", "SubscriberKey", sk));
if (email === "" || email === "null") {
return null; // early return — nothing to process
}
// ... rest of function
return email;
}
try {
var result = processSubscriber(sk);
if (result) {
Write(result);
}
} catch (e) {
Write("Error: " + e.message);
}
Show test script
<script runat="server">
/*
* Chapter: Throw / Early Return
* Proves:
* 1. throw Error (call-form) catchable; early return 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 processSubscriber(sk) {
if (!sk) { throw Error("SubscriberKey is required"); }
if (sk === "missing") { return null; }
return "ok:" + sk;
}
var msg = "";
try { processSubscriber(""); } catch (e) { msg = "" + e.message; }
assert("throw msg", msg, "SubscriberKey is required");
assert("early null", processSubscriber("missing") === null ? "null" : "other", "null");
assert("ok path", processSubscriber("abc"), "ok:abc");
</script>