Declaration

SSJS uses only var for variable declarations. let and const are not supported and will cause a runtime error.

var name = "Jane";
var count = 42;
var active = true;
var data = null;
var nothing; // undefined
Show test script
<script runat="server">
/*
 * Chapter: Declaration
 * Proves:
 *   1. var accepts string, number, boolean, null, and uninitialized values.
 *   2. Uninitialized var is undefined.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var name = "Jane";
var count = 42;
var active = true;
var data = null;
var nothing;
assert("string value", name, "Jane");
assert("number value", count, 42);
assert("boolean value", active, true);
assert("null value is null", data === null ? "null" : "other", "null");
assert("uninitialized is undefined", nothing === undefined ? "undefined" : "other", "undefined");
assert("uninitialized typeof undefined", "" + (typeof nothing), "undefined");
</script>

Scope

Variables declared with var have function scope, not block scope. A variable declared inside an if block or for loop is accessible outside that block.

if (true) {
    var x = 10; // function-scoped, not block-scoped
}
Write(x); // 10 — works fine

for (var i = 0; i < 3; i++) {
    // body
}
Write(i); // 3 — i is still accessible here

In SSJS, the “global scope” within a single <script runat="server"> block is the top-level scope. Variables declared at the top level of any script block are shared across all script blocks on the same page.

Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
 * Chapter: Scope
 * Proves:
 *   1. var inside if is visible outside the block.
 *   2. for-loop index remains visible after the loop.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
if (true) {
    var x = 10;
}
assert("var inside if is visible outside", x, 10);
for (var i = 0; i < 3; i++) { /* body */ }
assert("for index visible after loop", i, 3);
</script>

Hoisting

Function declarations are hoisted to the top of their containing scope. Variable declarations (not their values) are also hoisted.

// Function declarations are fully hoisted
greet(); // Works — even called before declaration

function greet() {
    Write("Hello!");
}

// Variable declarations are hoisted, but not their values
Write(y); // undefined (not an error, just undefined)
var y = 5;
Write(y); // 5

Hoisting across script blocks is NOT guaranteed. Always place function declarations in the first script block on a page.

Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
 * Chapter: Hoisting
 * Proves:
 *   1. Function declarations are callable before their source line.
 *   2. var is hoisted as undefined before assignment, then holds the value.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var greetOk = false;
try { greet(); greetOk = true; } catch (ex) { greetOk = false; }
function greet() { /* hoisted */ }
assert("function declaration callable before line", greetOk ? "true" : "false", "true");
assert("var before assignment is undefined", y === undefined ? "undefined" : "other", "undefined");
var y = 5;
assert("var after assignment is 5", y, 5);
</script>

Naming Conventions

SSJS follows standard JavaScript naming conventions. The SFMC community style guide recommends:

  • camelCase for local variables: subscriberKey, emailAddress, rowCount
  • PascalCase is reserved for platform objects: Platform, DataExtension, WSProxy
  • Prefix private/internal functions with _: _formatDate, _validateEmail
  • Constants (by convention only, no const): var MAX_ROWS = 200; (uppercase + underscores)
var subscriberKey = Platform.Request.GetQueryStringParameter("sk");
var emailAddress  = "";
var rowCount      = 0;

var MAX_RETRIES = 3;

function _formatName(first, last) {
    return first + " " + last;
}
Show test script
<script runat="server">
/*
 * Chapter: Naming Conventions
 * Proves:
 *   1. camelCase locals and UPPER_SNAKE convention constants are valid identifiers.
 *   2. Underscore-prefixed helpers are callable.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var subscriberKey = "sk";
var emailAddress = "";
var rowCount = 0;
var MAX_RETRIES = 3;
function _formatName(first, last) { return first + " " + last; }
assert("camelCase local", subscriberKey, "sk");
assert("empty string emailAddress", emailAddress, "");
assert("rowCount zero", rowCount, 0);
assert("MAX_RETRIES constant-by-convention", MAX_RETRIES, 3);
assert("_formatName helper", _formatName("Jane", "Smith"), "Jane Smith");
</script>

Multiple Variables

Declare multiple variables with separate var statements (one per variable is most readable and lint-friendly):

// Preferred: one var per line
var first  = "Jane";
var last   = "Smith";
var email  = "jane@example.com";

// Also valid: comma-separated (harder to diff)
var a = 1, b = 2, c = 3;
Show test script
<script runat="server">
/*
 * Chapter: Multiple Variables
 * Proves:
 *   1. One-var-per-line and comma-separated declarations both work.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var first = "Jane";
var last = "Smith";
var email = "jane@example.com";
var a = 1, b = 2, c = 3;
assert("first", first, "Jane");
assert("last", last, "Smith");
assert("email", email, "jane@example.com");
assert("comma a", a, 1);
assert("comma b", b, 2);
assert("comma c", c, 3);
</script>

Common Pitfalls

Forgetting var creates a global:

function doSomething() {
    result = "oops"; // ⚠️ No var — creates/overwrites global
}

Always use var inside functions to keep scope contained.

Variable leaking across iterations:

for (var i = 0; i < rows.length; i++) {
    var rowData = processRow(rows[i]); // rowData is re-declared each iteration
    // This works in SSJS but wouldn't in strict block-scoped languages
}
// rowData and i still accessible here
Show test script
<script runat="server">
/*
 * Chapter: Common Pitfalls
 * Proves:
 *   1. Assignment without var creates/overwrites a top-level binding.
 *   2. Loop var leak: i and rowData remain visible after the loop.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function doSomething() {
    result = "oops";
}
doSomething();
assert("assignment without var creates top-level", result, "oops");
var rows = [1, 2];
for (var i = 0; i < rows.length; i++) {
    var rowData = rows[i];
}
assert("loop index leaked", i, 2);
assert("loop var rowData leaked", rowData, 2);
</script>