SSJS Debugging & Error Handling
To catch, or not to catch, that is debugging.
When you start working with programmatic languages in Salesforce Marketing Cloud, you will quickly become close friends with the Error 500 page. And if you beat it and go forward with the code and official documentation, you will fall in love with unexpected errors in functions and API responses. How to survive those?
SSJS Testing Ground
Before diving deep into errors, let's talk about where to write the SSJS. The language is useful in many places within the Marketing Cloud ecosystem. Among else in:
- Cloud Pages (Web Studio)
- Code Resources (Web Studio -> Content Builder)
- Content Blocks (Content Builder)
- Emails and other communication (Email Studio / Mobile Studio / Content Builder)
- Script Activities (Automation Studio)
In all of the above cases, you might write more complex logic that will be error-prone and in need of debugging. However, only a few of those are really good to test your code, as only some of them provide access to methods mentioned later in the article.
Cloud Page
The most popular recommendation is Cloud Page - write or paste the SSJS code there, publish and check whether it is working correctly. It is a great way, as it allows you to easily leverage SSJS for the backend and HTML/CSS/JavaScript for the frontend.
It has, however, two flaws that, in some scenarios, might guide you to a different solution.
- Cost. Each view of the Cloud Page costs one Super Message. During debugging, you might hit quite a lot of those. And those tests - across your team and over time - stack up.
- Speed and context. Each time you want to republish the updated Cloud Page, you have to go through the Preview window (that load the whole code in POST method context), confirm it and go to the page via URL to see the GET method context.
Whenever you are writing an SSJS script on the Cloud Page, be sure to check your code on the published page, not the Web Studio Preview. Many SSJS functions do not work correctly in the Preview, so you might see errors that won't happen on the Cloud Page.
JSON Code Resource
When I want to go around those issues, I work in JSON Code Resource (in the past stored in Web Studio, now in Content Builder). It generates a link you can use to check whether everything works, just like a Cloud Page. However, page views are free. There is also no preview allowing for a faster save-reload cycle.
The cons? You won't be able to use any frontend (neither HTML nor JavaScript), which might be a deal-breaker in some scenarios.
So where?
If you need | Cloud Page | JSON Code Resource |
---|---|---|
SSJS | ✅ | ✅ |
HTML/CSS/JS | ✅ | ❌ |
No Cost | ❌ | ✅ |
Quick Save | ❌ | ✅ |
500 - Internal Server Error
The first type of error you might encounter is the dreaded 500 Error you see right after trying to run your code Reason? The backend code (SSJS/AMPScript) is invalid. There might be a few reasons for this. The most popular are:
- Typo in SSJS Function Name
- Unclosed or wrongly closed Bracket
- Use of JavaScript feature that is not available in SSJS (there is quite a lot of those...)
- Lack of declaration (in most cases either missing variable declaration,
Platform.Load("Core","1");
orvar soap = new Script.Util.WSProxy();
while using it later in the code)
How to deal with those issues? Apart from just reading through your code line by line, there are two faster solutions.
Divide and Conquer
The first one can be done directly in the Marketing Cloud but is a bit of a brute-force approach. You block half of your code from running by enclosing it in a multiline comment (/* here goes the code */
) and check again. If it works, you have half of the SSJS validated (from the 500 error perspective). If not, you split the remaining code in half and comment it out. Rinse, repeat. This way, you can quickly find the few lines that are the source of the issue and focus on validating them.
SSJS Linting
For many issues leading to 500 error, there is an even faster solution, but it requires some pre-work:
- You need to have Visual Studio Code with ESLint extension installed.
- You need to have NodeJS installed.
- You need to have a folder where you will store your SSJS code in files with the
.ssjs
file extension. - You need to install the excellent ESLint configuration for SSJS made by Joern Berkefeld. To do this, run in terminal
npm i -D eslint eslint-config-ssjs
for the SSJS folder.
Once done, you have automatic SSJS code validation (again, only from the perspective of 500 error - but it is still worth it).
There is also a much more basic and less helpful but built-in solution to validate your SSJS code directly in the Marketing Cloud. Script Activity in Automation Studio. After adding your code there and clicking Validate Syntax button, it will check it against some basic rules. However, I recommend going with the ESLint. It provides much more value and is faster once configured.
There will be a separate section about Try/Catch. It does not work for issues leading to 500 Error.
Write the Error down
You got past the 500 error. Great! The script is still not outputting what you wanted? It is time to check what else might be wrong. The easiest way is to use the Write
function. It shows whatever you pass in it to the frontend view. It is handy as SSJS does not support console.log due to its server-side nature (a little trick to make it work is below).
To use this function just add Write('This will be visible on the website')
between <script runat="server"> Platform.Load('Core','1');
and </script>
.
Of course, in real life, you will probably want to use it for variables:
<script runat="server">
Platform.Load('Core','1');
var response = HTTP.Get('http://www.example.com');
Write('Response from example.com: ' + response + '<br><br>');
</script>
As you can see in the code snippet above, I have more than just a response
variable within my Write
. The string before describes what is printed (useful mainly if you use multiple Writes in your code). The one after - <br><br>
- will separate it from the rest of the website content (however, as it is HTML, it won't work in JSON Code Resource. You can use \n\n
instead for the same outcome). I highly recommend this approach.
Write
is Core Library function, so you need to load it first in your script withPlatform.Load('Core','1');
.- If the variable you want to output is an object, you need to parse it to a string using
Stringify(response)
. - If you don't want to load Core Library, use Platform versions of those functions:
Platform.Response.Write()
andPlatform.Function.Stringify()
.
Debugging Variable
Whenever you create a script for the long-term, it is good to keep your debugging Write
functions. There might be new requirements. Data sources change. Marketing Cloud too. And sometimes, those things might break your code.
Of course, keeping the Writes
as above is a no-go. You don't want your customers to see those. But there is a neat little trick to eat the cake and have it too:
<script runat="server">
Platform.Load('Core','1');
var debugging = true;
var response = HTTP.Get('http://www.example.com?q=1234');
if (debugging) {
Write('Response from example.com: ' + response + '<br><br>');
};
</script>
I added two things:
- There is
var debugging = true;
near the top. When debugging, keep it ontrue
. When you publish it for production - change the value tofalse
. - The
Write
is within theif
block. It now works only if the debugging variable equalstrue
.
Make your life easier with a nice clean function that will save you a lot of time in the long run:
/**
* @function debugValue
* @description Outputs provided description and SSJS value to front-end in a type-safe & consistent way
* @param {string} description - Describes meaning of the second parameter in the output
* @param {*} value - The value that needs to be debugged
*/
function debugValue(description, value) {
Write(description + ': ' + (typeof value == 'object' ? Stringify(value) : value) + '<br><br>');
};
var response = HTTP.Get('http://www.example.com?q=1234');
if (debugging) debugValue('Response from example.com', response);
With this approach, once you copy-paste the function, you have cleaner debugging lines, less code to write, no worry whether you need to Stringify
or not and automatic new line addition for easier reading. Just pass debug description and the value you want to check and see the magic happen.
Console Logging
I mentioned before that SSJS does not support console.log
as it executes on a server. There is, however, a workaround that might bring the SSJS values to your console. SSJS personalization strings passed to your JavaScript can help:
console.log('Response from example.com' + '<ctrl:var name=response />')
If the variable you want to pass to JavaScript is an SSJS Object, you will need to create a parsed variable to make it work:
- In SSJS create
var parsedResponse = Stringify(response);
- In JS change the personalization to
console.log(<ctrl:var name=parsedResponse />)
(notice differentname
value and lack of quotes around personalization string)
Alternatively use ternary to cover all scenarios:
var parsedResponse = typeof response == 'object' ? Stringify(response) : response
Try to Catch the Error
Using Write
is a great and simple solution, but it will work only if the code runs correctly. And sometimes it won't. For those cases, add a Try-Catch block. Check the Response tab below to compare the difference between standard Write(response)
and writing the caught error.
- Request
- Response
<script runat="server">
Platform.Load('Core','1');
var debugging = true;
/**
* @function debugValue
* @description Outputs provided description and SSJS value to front-end in a type-safe & consistent way
* @param {string} description - Describes meaning of the second parameter in the output
* @param {*} value - The value that needs to be debugged
*/
function debugValue(description, value) {
Write(description + ': ' + (typeof value == 'object' ? Stringify(value) : value) + '<br><br>');
};
// Try/Catch Block with error Write
try {
var response = HTTP.Get('http://www.example.c');
} catch (error) {
if (debugging) debugValue('1. Error caught', error);
};
// Standard Write
if (debugging) debugValue('2. Response from example.com', response);
</script>
1. Error caught: {"message":"An error occurred when attempting to evaluate a HTTPGet function call. See inner exception for details.\r\n ClientID: 518000476\r\n JobID: 0\r\n ListID: 0\r\n BatchID: 0\r\n SubscriberID: 0\r\n URL: http://www.example.c\r\n","description":"ExactTarget.OMM.FunctionExecutionException: An error occurred when attempting to evaluate a HTTPGet function call. See inner exception for details.\r\n ClientID: 518000476\r\n JobID: 0\r\n ListID: 0\r\n BatchID: 0\r\n SubscriberID: 0\r\n URL: http://www.example.c\r\n Error Code: OMM_FUNC_EXEC_ERROR\r\n - from Jint --> \r\n\r\n --- inner exception 1---\r\n\r\nExactTarget.OMM.OMMException: An exception occurred when attempting to retrieve content by a HttpGet call. URL: http://www.example.c\r\n Error Code: HTTP_WB_RTV\r\n - from OMMCommon --> \r\n\r\n --- inner exception 2---\r\n\r\nSystem.Net.WebException: The remote name could not be resolved: 'www.example.c' - from System\r\n\r\n\r\n\r\n\r\n\r\n"}
2. Response from example.com: undefined
As you can see, this response is quite long. The most important information is in the last sentence of first paragraph. In this example we learn that System.Net.WebException: The remote name could not be resolved: 'www.example.c' - from System
. It makes it easy to understand that there is issue with the URL we provided.
You can also see why Try/Catch block is so valuable, as in second paragraph our Write
printed undefined
, which isn't very helpful. This is due to the fact, that because of the invalid URL, the GET did not return to response variable.
Try/Catch is especially useful for SSJS Functions that call out-of-page data. Marketing Cloud Data Extensions, Salesforce Objects or even data sources from outside the Salesforce environment. It also will help when you are handling responses from such sources. For example, trying to get value from a nested object.
In most cases, the simple Try/Catch will be all you need, but there is also a third element that might be very helpful: finally
. Its purpose is to store a code that will execute, no matter what. Great for a cleanup code that must run regardless whether the previous logic was successful or not.
try {
// Risky code
} catch (error) {
// Code that will be executed if the risky code crashes
} finally {
// Code that will be executed always
};
Out of the three, SSJS requires only Try. You can use it with Catch, Finally or both, depending on your use case.
Try/Catch block in SSJS has separate scope. It means that any variables declared within the block won't be accessible from outside. Neither in the other parts of SSJS nor by personalization strings.
There is a way to overcome this limitation. Declare the variables you want to use globally before the try
and modify their values within the block:
<script runat="server">
Platform.Load('Core','1');
var response;
try {
response = HTTP.Get('http://www.example.com');
} catch (error) {
// Error handling logic
};
</script>
Throw Error
You can leverage the Try/Catch block further by implementing your custom errors with throw
. Why would you break your own code on purpose? It will help you control the execution of the code - especially if it uses external data. It can also be great for debugging and streamlining user experience.
try {
if (submissionData.greCaptcha) {
var secret = "XXX";
var payload = "secret=" + secret + "&response=" + submissionData.greCaptcha;
var contentType = "application/x-www-form-urlencoded";
var endpoint = "https://www.google.com/recaptcha/api/siteverify";
var response = HTTP.Post(endpoint, contentType, payload);
if (response.StatusCode == 200) {
var parsedResponse = Platform.Function.ParseJSON(String(response.Response));
if (!parsedResponse.success) throw 'Wrong reCAPTCHA';
} else {
throw 'reCAPTCHA API error';
};
};
} catch (error) {
debugValue('reCaptcha Error', error);
};
In the above example, you can see that we are checking whether the response from our reCaptcha validation to know whether we reached the Google server (else we throw reCAPTCHA API error
) and if yes - was the Captcha solved.
Here I'm using the string passed to throw
just for debugging purposes, but you can use it for an overlay with feedback to your client or log the error.
You are not limited to strings with the throw
. You can also pass objects to mimic the Marketing Cloud errors. It will make error handling much easier:
if (response.StatusCode != 200) {
throw {'message': 'HTTP.Get Error', 'description': 'Could not connect to example.com'};
};
Error Logging
Once you debug your code using the above methods, it is still good to program defensively and leverage the Try/Catch block in the production environment. It will allow you to handle the errors for the customers and log the errors to a data extension (it will provide you with a history of problems along with a description of what happened). You can even build something more sophisticated, like automatically creating a JIRA bug ticket.
Logging to Data Extension
The easiest way to log your errors is to leverage Marketing Cloud Data Extensions.
My recommendation is to create one Data Extension that will capture all the errors from the whole instance. It will make it much easier and faster to check any new issues.
Here you can find a sample setup:
Column Name | Primary Key | Length | Type | Nullable | Default Value |
---|---|---|---|---|---|
id | Yes | 36 | Text | No | |
errorSource | 100 | Text | No | ||
errorMessage | 2000 | Text | Yes | ||
errorDescription | 2000 | Text | Yes | ||
errorDate | Date | Yes | Current Date |
If you have multiple Business Units on your instance, add it as a Shared Data Extension.
It shouldn't be sendable or testable, but consider adding a retention period. Align it with the internal process for checking it for new issues. Seven days per record is a good starting point.
Global Error Catching
Instead of writing Try/Catch blocks for every potentially risky function, you might create just one block that will capture the whole code. You can even use it to catch errors in your AMPScript, as described by Zuzanna Jarczyńska.
This approach might make your code shorter and more comfortable to read but limit your control over specific handling for various scenarios.
Any redirect within your try
block is recognized as an error and caught. If you need to use a redirect, don't use the global Try/Catch approach and instead leverage it only for potentially problematic code.
You can also leverage one of the approaches mentioned by Gortonington in his article on Try/Catch. It might be instrumental when you want to leverage redirect to handle the customer's error.
Error Handler Function
Once you start implementing the solutions mentioned above, it might get quite repetitive to add the same lines for conditional Write
, log to Data Extension, et cetera.
You can solve it by creating a single Error Handler function and just calling it wherever it is needed:
<script runat="server">
Platform.Load('Core','1');
var debugging = true;
/**
* @function debugValue
* @description Outputs provided description and SSJS value to front-end in a type-safe & consistent way
* @param {string} description - Describes meaning of the second parameter in the output
* @param {*} value - The value that needs to be debugged
*/
function debugValue(description, value) {
Write(description + ': ' + (typeof value == 'object' ? Stringify(value) : value) + '<br><br>');
};
/**
* @function handleError
* @description If debuging is enabled, outputs the error value. Else, adds the error with context to error logging Data Extension.
* @param {Object} error - The caught error object. Can come from the try/catch block or be manually created.
* @param {string} error.message - First error key stores short error message describing the issue.
* @param {string} error.description - Second error key stores detailed error path helping with root cause analysis
*/
function handleError(error) {
if (debugging) {
debugValue('Found error', error);
} else {
// Remember that if your Logging Data Extension is in Shared Folder, you need to add the "ENT." prefix to name
Platform.Function.InsertData(
'Data_Extension_Name',
['id', 'errorSource', 'errorMessage', 'errorDescription'],
[GUID(), 'Name of the asset where the script runs', Stringify(error.message), Stringify(error.description)]
);
};
};
try {
var response = HTTP.Get('http://www.example.c');
} catch (error) {
handleError(error);
};
</script>
Whenever the Try/Catch block finds an error, the above snippet will check the value of debugging variable. If it is equal to true
, it will just Write the error to the page to make it easier to debug quickly. If it is false
, it will add the error to the Data Extension instead.
Most of the Marketing Cloud Errors are objects with two keys: message
and description
. Putting those elements into separate columns of the Data Extension make it easier to read the log.
Using the Error Handler Function is useful, especially when you are triggering it multiple times or when you have elaborate logic for handling (for example, API calls to external systems for bug tracking).
You can also create your custom error, by passing a custom object to the handleErrror
function:
handleError({message: 'Custom Error Message', description: 'Custom Error Description'});
There is also a new solution - SSJS Lib - created by email360. Among other tools, they support console logging and simplified debugging. You can read more about the implementation here.
Sum Up
If you have any problems with your SSJS code:
- Test it in Cloud Page or JSON Code Resource 🔗
- Overcome 500 Error with linting or divide & conquer methodology 🔗
- Use the
Write
function to understand what happens with your variables during execution 🔗 - Use the Try/Catch block to understand better what is the reason for errors you encounter 🔗
- Save errors from your production code to easily track and solve the problems 🔗
- Simplify the approach with custom Error Handler Function 🔗