Reading a NetSuite stack trace
UNEXPECTED_ERROR is NetSuite's way of saying it doesn't know either. The information you need is usually sitting right next to it.
A Suitelet returns a page full of zeroes. No visible failure — every number is simply 0. The execution log has this:
ERROR Error in getTransactionData
{"type":"error.SuiteScriptError",
"name":"UNEXPECTED_ERROR",
"message":"An unexpected SuiteScript error has occurred",
"stack":["Error\n at getTransactionData (/SuiteScripts/usage.js:574:55)
\n at Object.onRequest (/SuiteScripts/usage.js:236:40)"]}
And a second entry, a moment earlier:
ERROR Error calculating monthly usage
ReferenceError: searchResult is not defined
Start with the second error, not the first
The instinct is to chase UNEXPECTED_ERROR because it looks more serious. It isn't — it's the least informative thing in the log. UNEXPECTED_ERROR is what NetSuite returns when the underlying exception doesn't map to one of its own error types. It's a shrug.
The ReferenceError is the real signal. It names a variable, and variable names are searchable. searchResult is not defined means the code referenced searchResult before anything assigned to it.
Nine times out of ten in SuiteScript, that's a search that was created but never run:
var usageSearch = search.create({
type: search.Type.TRANSACTION,
filters: filters,
columns: [ search.createColumn({ name: 'quantity', summary: 'SUM' }) ]
});
// missing:
// var searchResult = usageSearch.run().getRange({ start: 0, end: 1 });
var totalUsage = 0;
if (searchResult && searchResult.length > 0) { // searchResult never existed
...
}
search.create() builds a search object. It doesn't execute anything. The results only exist after .run(), and it's easy to lose that line during a refactor because the code around it still reads correctly.
Why everything came back zero instead of failing
This is the detail worth internalising. totalUsage was initialised to 0 before the guard. The guard threw, the surrounding try/catch swallowed it, and the function returned its initialised value.
A defensive default turned a hard failure into a plausible wrong answer. Nobody would have noticed if a person hadn't thought the numbers looked low.
0 and catching broadly is how calculation bugs become silent. If a number can't be computed, returning null and letting the caller decide is safer than returning a number that looks real.
Using the line and column numbers
The trace gives you usage.js:574:55 — line 574, column 55. The column is the part people skip, and on a line with several chained calls it tells you which one failed.
Two cautions specific to NetSuite. The line numbers refer to the deployed file in the File Cabinet, which may not be what's open in your editor if a deploy failed halfway. And if the script was minified or assembled by a build step, the numbers refer to the built artifact, not your source.
The second frame matters too. onRequest (usage.js:236) tells you which entry point was running, which narrows things immediately when a Suitelet handles several actions through one onRequest.
Read the debug lines around the error
In this case the log also contained, just above the failure:
DEBUG Location count for item: 0
DEBUG Transaction search filters: [["item","anyof","6711"],"AND",
["trandate","within","2024-06-19","2025-06-19"],"AND",
["mainline","is","F"], ...]
Those two lines rule out a whole branch of investigation. The filters are well-formed and the item ID is populated, so this isn't a bad-parameter problem — the inputs were fine and the execution was broken. That's ten minutes saved.
A defensive default turned a hard failure into a plausible wrong answer.
A working order
- Sort the log by time and read the earliest error in the sequence, not the loudest.
- Prefer any error that names a variable, field, or operator over a generic one.
- Use the line and column to find the statement, then check what was supposed to assign to that variable.
- Read the debug entries immediately before the failure — they tell you what the inputs were.
- Ask whether the failure was swallowed. If the output looks wrong rather than absent, something caught it.
Related: why a logging call itself can be the thing that's throwing.