SFMC’s native error handling is minimal and hard to inspect. The most reliable way to debug production issues is to log errors yourself to a Data Extension.

Setting Up an Error Log DE

Create a DE with these columns:

Column Type Length Notes
LogId Text 36 Primary key, set to GUID()
Timestamp Date When the error occurred
Page Text 255 CloudPage name or script identifier
ErrorMessage Text 500 Error text from String(e)
RequestData Text 4000 Query string, POST body snapshot
SubscriberKey Text 254 If available
Severity Text 20 "error", "warning", "info"

There is deliberately no stack-trace column: the SSJS engine exposes no .stack property on any error shape, so such a column could only ever store undefined. Capture the error text with String(e) instead — it works for engine-raised errors, new Error(...) (whose .message reads back undefined), and call-form Error(...) alike.


Show test script
<script runat="server">
/*
 * Chapter: Setting Up an Error Log DE
 * Proves:
 *   1. NON-ASSERTABLE: DE schema setup in UI.
 *   GUID/Now available for log rows.
 * 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("GUID typeof", typeof Platform.Function.GUID, "clrmethodinfo");
var now = Platform.Function.Now();
assert("Now usable", now !== null && now !== undefined ? "ok" : "bad", "ok");
</script>

Basic Error Logger

function logError(page, message, requestData, subKey) {
    try {
        Platform.Function.InsertData(
            "ErrorLog",
            ["LogId", "Timestamp", "Page", "ErrorMessage", "RequestData", "SubscriberKey", "Severity"],
            [
                Platform.Function.GUID(),
                Platform.Function.Now(),
                page || "unknown",
                (message || "").substring(0, 500),
                (requestData || "").substring(0, 4000),
                subKey || "",
                "error"
            ]
        );
    } catch(e) {
        // Silently fail — don't error while logging an error
    }
}

Show test script
<script runat="server">
/*
 * Chapter: Basic Error Logger
 * Proves:
 *   1. log function Stringify + GUID shape without DE write.
 * 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 buildLogRow(page, severity, message, data) {
    return {
        id: Platform.Function.GUID(),
        ts: Platform.Function.Now(),
        page: page,
        severity: severity,
        message: (message || "").substring(0, 500),
        data: Platform.Function.Stringify(data || {}).substring(0, 200)
    };
}
var row = buildLogRow("test", "error", "boom", { a: 1 });
assert("id non-empty", ("" + row.id).length > 0 ? "true" : "false", "true");
assert("severity", row.severity, "error");
assert("data has a", row.data.indexOf("a") >= 0 ? "true" : "false", "true");
</script>

Page-Level Error Wrapper

Wrap your entire CloudPage in a try/catch:

<script runat="server">
var PAGE_NAME = "checkout-form";
var requestSnapshot = "method=" + Platform.Request.Method
    + " qs=" + Platform.Request.RequestURL;

try {
    Platform.Load("core", "1.1.5");
    var rawBody = Platform.Request.GetPostData();
    requestSnapshot += " body=" + (rawBody || "").substring(0, 200);

    // === Main page logic here ===

    var email = Platform.Request.GetFormField("email");
    if (!Platform.Function.IsEmailAddress(email)) {
        throw new Error("Invalid email: " + email);
    }

    // ... rest of logic ...

} catch(e) {
    // String(e) — .message is undefined for new Error(/* ... */) and .stack does not exist
    logError(PAGE_NAME, String(e), requestSnapshot, "");

    Platform.Response.ContentType = "application/json";
    Write(Platform.Function.Stringify({ error: "An error occurred. Please try again." }));
}

function logError(page, message, requestData, subKey) {
    try {
        Platform.Function.InsertData(
            "ErrorLog",
            ["LogId", "Timestamp", "Page", "ErrorMessage", "RequestData", "SubscriberKey", "Severity"],
            [Platform.Function.GUID(), Platform.Function.Now(), page,
             (message || "").substring(0, 500),
             (requestData || "").substring(0, 4000), subKey || "", "error"]
        );
    } catch(e) {}
}
</script>

Show test script
<script runat="server">
/*
 * Chapter: Page-Level Error Wrapper
 * Proves:
 *   1. try/catch wrapper returns safe JSON shape.
 * 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); }
}
Platform.Load("core", "1.1.5");
function safeRun(fn) {
    try { return { ok: true, value: fn() }; }
    catch (e) { return { ok: false, error: String(e) }; }
}
var r = safeRun(function () { throw new Error("x"); });
assert("caught", r.ok ? "true" : "false", "false");
assert("error has x", r.error.indexOf("x") >= 0 ? "true" : "false", "true");
</script>

Severity Levels

Use different severity levels for different types of events:

function log(page, severity, message, data) {
    try {
        Platform.Function.InsertData(
            "AppLog",
            ["LogId", "Timestamp", "Page", "Severity", "Message", "Data"],
            [Platform.Function.GUID(), Platform.Function.Now(), page, severity,
             (message || "").substring(0, 500), Platform.Function.Stringify(data || {}).substring(0, 2000)]
        );
    } catch(e) {}
}

log("orders", "info", "Order received", { orderId: "123", total: 99.99 });
log("checkout", "warning", "Duplicate submission detected", { email: email });
log("payment", "error", "Payment gateway timeout", { gateway: "stripe", elapsed: elapsed });

Show test script
<script runat="server">
/*
 * Chapter: Severity Levels
 * Proves:
 *   1. severity tokens as documented.
 * 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 levels = { info: 1, warn: 2, error: 3, fatal: 4 };
assert("error > warn", levels.error > levels.warn ? "true" : "false", "true");
</script>

Automation Studio Logging

For automations, add structured step logging:

var JOB_ID = Platform.Function.GUID();
var SCRIPT_NAME = "nightly-sync";

function logStep(step, status, message, count) {
    Platform.Function.InsertData(
        "AutomationLog",
        ["JobId", "Script", "Step", "Status", "Message", "RowCount", "Timestamp"],
        [JOB_ID, SCRIPT_NAME, step, status, message || "", count || 0, Platform.Function.Now()]
    );
}

logStep("start", "running", "Script started", 0);
// ... process rows ...
logStep("retrieve", "ok", "Rows retrieved", rows.length);
// ... process each row ...
logStep("complete", "ok", "All rows processed", processedCount);

Show test script
<script runat="server">
/*
 * Chapter: Automation Studio Logging
 * Proves:
 *   1. InsertData typeof (write NON-ASSERTABLE).
 * 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("InsertData typeof", typeof Platform.Function.InsertData, "clrmethodinfo");
</script>

HTTP Error Logging

Log failed API calls with full context:

function callApi(url, method, payload, token) {
    var req = new Script.Util.HttpRequest(url);
    req.method = method || "GET";
    req.setHeader("Authorization", "Bearer " + token);
    if (payload) {
        req.contentType = "application/json";
        req.postData = Platform.Function.Stringify(payload);
    }

    var resp;
    try {
        resp = req.send();
    } catch(e) {
        logError("api-call", "HTTP request failed: " + url + "" + String(e), "", "");
        throw e;
    }

    // statusCode is a CLR value — Number() converts it to a real JavaScript number
    var status = Number(resp.statusCode);
    if (status >= 400) {
        log("api-call", "error",
            "HTTP " + status + " from " + url,
            { url: url, status: status, body: String(resp.content).substring(0, 500) }
        );
    }

    return resp;
}

Show test script
<script runat="server">
/*
 * Chapter: HTTP Error Logging
 * Proves:
 *   1. HttpRequest constructs; Stringify payload.
 * 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 req = new Script.Util.HttpRequest("https://example.com/");
assert("HttpRequest typeof", typeof req, "clr");
assert("Stringify payload", Platform.Function.Stringify({ e: 1 }).indexOf("e") >= 0 ? "true" : "false", "true");
</script>

Monitoring with Email Alerts

For critical automation failures, send an alert email:

} catch(e) {
    // String(e) — .message is undefined for new Error(/* ... */) and .stack does not exist
    logError(SCRIPT_NAME, String(e), "", "");

    // Alert the ops team
    try {
        Platform.Load("core", "1.1.5");
        var ts = TriggeredSend.Init("AutomationError_TSD");
        ts.Send({
            EmailAddress: "ops@yourbrand.com",
            SubscriberKey: "ops-alert",
            Attributes: {
                ScriptName: SCRIPT_NAME,
                ErrorMessage: e.message,
                Timestamp: Platform.Function.Now()
            }
        });
    } catch(alertErr) {
        // Fail silently if alert itself fails
    }
}
Show test script
<script runat="server">
/*
 * Chapter: Monitoring with Email Alerts
 * Proves:
 *   1. NON-ASSERTABLE: TriggeredSend in this harness.
 * 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("guidance-only", "documented", "documented");
</script>

See Also