Function Declarations

Function declarations are hoisted to the top of their scope:

// Can call before declaration thanks to hoisting
greet("Jane");

function greet(name) {
    Write("Hello, " + name + "!");
}
Show test script
<script runat="server">
/*
 * Chapter: Function Declarations
 * Proves:
 *   1. declaration 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 add(a, b) { return a + b; } assert("add", add(2, 3), 5);
</script>

Function Expressions

Assigned to variables — not hoisted:

var greet = function(name) {
    Write("Hello, " + name + "!");
};

greet("Jane"); // Must be called AFTER the assignment
Show test script
<script runat="server">
/*
 * Chapter: Function Expressions
 * Proves:
 *   1. expression 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");
}
var mul = function (a, b) { return a * b; }; assert("mul", mul(4, 5), 20);
</script>

Parameters and Return Values

function add(a, b) {
    return a + b;
}

// Default parameter simulation (no default params in SSJS)
function greet(name, greeting) {
    greeting = greeting || "Hello";
    name = name || "Subscriber";
    return greeting + ", " + name + "!";
}

// Variable arguments (no rest params)
function sum() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}
sum(1, 2, 3); // 6
Show test script
<script runat="server">
/*
 * Chapter: Parameters
 * Proves:
 *   1. arguments + return.
 * 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 arity() { return arguments.length; }
function first() { return arguments[0]; }
assert("argc", arity(1, 2, 3), 3);
assert("arg0", first("x"), "x");
function greet(name) { return "Hello, " + name + "!"; }
assert("return", greet("World"), "Hello, World!");
</script>

Arrow Functions — NOT SUPPORTED

Arrow functions are ES6 and will throw a runtime error:

// ❌ Not supported in SSJS
var double = (x) => x * 2;
var greet  = name => "Hello, " + name;

// ✅ Use function expressions instead
var double = function(x) { return x * 2; };
var greet  = function(name) { return "Hello, " + name; };
Show test script
<script runat="server">
/*
 * Chapter: Arrow NOT SUPPORTED
 * Proves:
 *   1. Negative guidance only.
 *   NON-ASSERTABLE: arrow syntax aborts parse.
 * 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");
}
assert("use-function-keyword", "ok", "ok");
</script>

Closures

Closures work as in standard JavaScript — inner functions capture references to outer scope:

function makeCounter(start) {
    var count = start || 0;
    return function() {
        count++;
        return count;
    };
}

var counter = makeCounter(10);
Write(counter()); // 11
Write(counter()); // 12
Write(counter()); // 13

Closures are commonly used for configuration objects:

function createLogger(prefix) {
    // assign then return — avoid returning an object literal directly (see below)
    var api = {
        log: function(msg) {
            Write("[" + prefix + "] " + msg + "<br>");
        },
        error: function(msg) {
            Write("[" + prefix + " ERROR] " + msg + "<br>");
        }
    };
    return api;
}

var logger = createLogger("SSJS");
logger.log("Starting process");
logger.error("Something went wrong");
Show test script
<script runat="server">
/*
 * Chapter: Closures
 * Proves:
 *   1. counter + logger.
 * 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 makeCounter() { var n = 10; return function () { n++; return n; }; }
var c = makeCounter(); assert("11", c(), 11); assert("12", c(), 12);
function createLogger(prefix) { var api = { log: function (msg) { return "[" + prefix + "] " + msg; } }; return api; }
assert("log", createLogger("SSJS").log("hi"), "[SSJS] hi");
</script>

The Module Pattern (SFMC Best Practice)

Because SSJS lacks classes, modules, or proper encapsulation, the Revealing Module Pattern is the recommended approach for building reusable utilities:

/**
 * Creates a DataExtension helper module.
 * @param {string} deName - Data Extension name
 * @returns {{ lookup: Function, upsert: Function }}
 */
function DEHelper(deName) {
    Platform.Load("core", "1.1.5"); // required for DataExtension.Init
    var service = {
        lookup: lookup,
        upsert: upsert,
        count:  count
    };

    return service;

    function lookup(returnField, filterField, filterValue) {
        return Platform.Function.Lookup(deName, returnField, filterField, filterValue);
    }

    function upsert(keyFields, keyValues, dataFields, dataValues) {
        return Platform.Function.UpsertData(deName, keyFields, keyValues, dataFields, dataValues);
    }

    function count() {
        // no Platform function returns a row count — retrieve and measure instead
        return DataExtension.Init(deName).Rows.Retrieve().length;
    }
}

// Usage
var subscribers = DEHelper("Subscribers");
var email = subscribers.lookup("Email", "SubscriberKey", sk);
subscribers.upsert(["SubscriberKey"], [sk], ["LastSeen"], [Platform.Function.Now()]);

Key properties of this pattern:

  • Calling DEHelper() without new is safe — no this binding issues
  • Returns a plain object of named functions — easy to extend and test
  • Inner functions capture deName in closure — no global state needed
  • Never use return { method: function() {...} } directly — SSJS has a bug where returning an object literal from a function can fail. Use the pattern above (assign to variable, return variable) instead.
Show test script
<script runat="server">
/*
 * Chapter: Module Pattern
 * Proves:
 *   1. revealing module.
 * 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 DEHelper(name) { var service = { lookup: lookup }; return service; function lookup() { return name; } }
assert("name", DEHelper("Subscribers").lookup(), "Subscribers");
</script>

Object Literal Return Bug

This is a known SSJS engine limitation:

// ❌ May fail in some SSJS contexts
function getConfig() {
    return {
        timeout: 30,
        retries: 3
    };
}

// ✅ Assign to variable first, then return
function getConfig() {
    var config = {
        timeout: 30,
        retries: 3
    };
    return config;
}
Show test script
<script runat="server">
/*
 * Chapter: Object Literal Return Bug
 * Proves:
 *   1. assign-then-return 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 getConfigSafe() { var cfg = { timeout: 30, retries: 3 }; return cfg; }
var c = getConfigSafe(); assert("timeout", c.timeout, 30);
</script>

Recursive Functions

Recursion works normally, but SSJS has stack limits. Keep recursion depth reasonable (< 100 levels):

function factorial(n) {
    if (n <= 1) { return 1; }
    return n * factorial(n - 1);
}

Write(factorial(10)); // 3628800
Show test script
<script runat="server">
/*
 * Chapter: Recursion
 * Proves:
 *   1. factorial(10).
 * 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 factorial(n) { if (n <= 1) return 1; return n * factorial(n - 1); }
assert("fact10", factorial(10), 3628800);
</script>

IIFE (Immediately Invoked Function Expression)

Useful for creating a private scope:

(function() {
    var privateVar = "not visible outside";
    
    // All code here is scoped
    var result = Platform.Function.Lookup("Config", "Value", "Key", "timeout");
    
    // Only expose what's needed
    Platform.Variable.SetValue("@timeout", result);
})();
Show test script
<script runat="server">
/*
 * Chapter: IIFE
 * Proves:
 *   1. IIFE isolates scope.
 * 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 out = (function () { var secret = 7; return secret * 2; })();
assert("iife", out, 14);
assert("secret not leaked", typeof secret, "undefined");
</script>