Open any NetSuite account with custom scripts and you will find the same problem: scripts that fail without telling anyone. A scheduled script throws an unexpected error on row 847 of a CSV import. The execution log shows "Unexpected Error" with no context. The operations team discovers the issue three days later when a customer calls about a missing invoice.

This is not a SuiteScript limitation. It is a development standards problem. SuiteScript 2.1 provides everything you need to build robust error handling. Most developers simply do not use it. Worse, a lot of what gets published as "best practice" online is surface-level (wrap everything in try-catch, log to console) and does not survive contact with production NetSuite at scale.

The pattern: three layers of defence

Our development framework enforces a three-layer error handling pattern across every script type. No script passes code review without all three layers present.

Layer 1: Context-rich structured logging with correlation IDs

Every function that performs an I/O operation must be wrapped in a try-catch block. But the catch block must do four things: capture context, correlate across script boundaries, respect governance, and distinguish recoverable from unrecoverable failures.

The structured log entry must include a correlation ID (a UUID generated at the entry point and passed through every downstream function), the stage of execution, the affected record type and ID, and a classification (recoverable, non-recoverable, or data-integrity).

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/log', 'N/runtime'],
(record, log, runtime) => {

    // correlationId is generated at the entry point and
    // passed into downstream helpers via a shared context object
    const correlationId = generateUuid();

    try {
        const poRecord = record.load({
            type: record.Type.PURCHASE_ORDER,
            id: poId,
            isDynamic: false  // lighter governance than dynamic
        });
        // ... business logic
    } catch (e) {
        log.error({
            title: 'PO_RATE_UPDATE_FAILED',
            details: JSON.stringify({
                correlationId: correlationId,
                poId: poId,
                stage: 'record_load',
                classification: e.name === 'RCRD_DSNT_EXIST'
                    ? 'data-integrity'
                    : 'recoverable',
                errorName: e.name,
                errorMessage: e.message,
                governanceRemaining: runtime.getCurrentScript().getRemainingUsage()
            })
        });
        throw e; // always re-throw; let the orchestration layer decide
    }
});

The correlation ID matters because a single business operation often spans multiple script executions. A User Event fires on record save, which triggers a Workflow Action Script, which calls a RESTlet, which queues a Map/Reduce. Without a correlation ID threaded through all of them, you cannot reconstruct what went wrong from the execution log alone. With one, you run a single saved search filtered by correlation ID and see the whole chain.

The governanceRemaining capture is the tell for scripts failing on unit limits rather than logic errors. If you see a pattern of errors all occurring when remaining units drop below 200, you have a governance problem dressed up as a data problem.

The classification field drives the layer-2 notification logic: data-integrity errors page the operations team immediately, non-recoverable errors email with SLA, recoverable errors accumulate for a daily digest.

Layer 2: Idempotency and retry semantics

Most SuiteScript error handling advice stops at "log the error." It misses the harder question: what happens on retry?

A Map/Reduce script processing 10,000 records throws an error on record 4,832. NetSuite's yield mechanism will retry the failed key. If your map function has already created a custom record, updated an inventory balance, or sent an email before the error, the retry will do it again. Now you have duplicate custom records and doubled inventory adjustments.

The pattern we enforce: every write operation in a script that can be retried must be idempotent. Before creating a custom record, search for an existing one with the same natural key. Before updating inventory, check whether this specific transaction has already been applied (via a processed-flag field on the source record). Before sending an email, record that the email was sent and check before re-sending.

For scheduled scripts that queue work for Map/Reduce scripts, add a checkpoint pattern: write a "batch started" record before queueing, update it on completion, and check for incomplete batches on the next scheduled run. This handles the case where a scheduled script itself dies mid-execution, which no try-catch can catch.

For operational notification, route classification determines the channel. Data-integrity errors go to a Teams or Slack webhook for immediate attention. Non-recoverable errors email a configured group with a 4-hour SLA commitment. Recoverable errors aggregate into a daily digest saved search. This prevents notification fatigue, which is the second most common cause of missed incidents after no notification at all.

Layer 3: The error log record with resolution workflow

For scripts that process high volumes or run on schedules, we create entries in a custom error log record (customrecord_sg_error_log). This is not just a log. It is a workflow.

The record stores the correlation ID, the script ID and deployment ID, the error timestamp, the affected record type and ID, the error classification, the error message, the execution context snapshot (governance remaining, current user, script stage), and a status field: New, Acknowledged, Resolved, Suppressed.

The Suppressed status is the one most developers overlook. In any mature NetSuite environment, there will be known error patterns that cannot be fixed (third-party integration timeouts during vendor maintenance windows, for example) but must still be visible. Suppressed errors are visible in the log but excluded from notification counts. This prevents the team from either drowning in noise or becoming desensitised to it.

Paired with this, a saved search dashboard for operations showing: New errors in the last 24 hours, errors awaiting acknowledgement beyond SLA, and mean time to resolution trend over 30 days. The dashboard is the single source of truth for system health.

Script-type-specific considerations

The three layers apply universally, but the implementation varies by script type.

Map/Reduce scripts require special attention because they process records in batches. An error in the map stage for one record should not halt processing of all remaining records. Use the summarize stage to collect all errors from the map and reduce stages, then send a single consolidated notification rather than one email per failed record.

User Event scripts on the beforeSubmit entry point can prevent a save. If your error handling throws an error here, the user loses their work. For non-critical operations in beforeSubmit, catch the error, log it, notify the team, but allow the save to proceed. Reserve hard failures for data integrity issues only.

RESTlet scripts must return structured error responses, not throw uncaught exceptions. An external system calling your RESTlet needs a JSON response with an error code and message it can parse, not an HTTP 500 with a stack trace.

What we see in code reviews

The most common error handling failures we encounter during rescue engagements are: empty catch blocks that swallow errors entirely; catch blocks that log the error but do not re-throw or notify; error messages that say "An error occurred" with no context; and Map/Reduce scripts with no summarize stage implementation.

Each of these is a production incident waiting to happen. The fix is not complex. It simply requires the discipline to treat error handling as a first-class requirement rather than an afterthought.

Enforcing the standard

A pattern is only useful if it is enforced. In our practice, error handling coverage is a mandatory item on the code review checklist. Every function that performs an I/O operation (record load, record save, search, HTTP call) must have all three layers present. If it does not, the code is returned for revision before it reaches UAT.

Want a code review of your existing SuiteScript library?

Book a Free Consultation