Error Logging
Capture and persist SSJS errors to Data Extensions for debugging, monitoring, and alerting — with reusable logging patterns.
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>