NetSuite · Map/Reduce · 7 August 2026

MAP_REDUCE_ALREADY_RUNNING is not an error

Show it to a user as a failure and they will resubmit. The work was already queued, so now it happens twice.

The failure that wasn't

A Suitelet lets warehouse staff select orders and process them in batches. Selecting orders writes batch records, then fires a Map/Reduce to do the work.

Two people are working the queue. The second one submits while the first batch is still running, and gets a red error toast:

MAP_REDUCE_ALREADY_RUNNING

So they do the sensible thing. They wait a moment and submit again. Same error. They ask a colleague, who suggests trying it later. Eventually the first batch finishes, the resubmit works, and now some orders have been processed twice.

Every part of that is a reasonable response to being told something failed. The problem is that nothing had failed.

What the exception actually describes

Look at the order of operations:

const batchIds = storeBatches(soIds, filters, location, mode);   // records written

task.create({
    taskType:     task.TaskType.MAP_REDUCE,
    scriptId:     MR_SCRIPT,
    deploymentId: MR_DEPLOY,
    params:       { custscript_batch_ids: JSON.stringify(batchIds) }
}).submit();                                                      // throws

The batch records were already saved before the task submit was attempted. The user's work is recorded. And the Map/Reduce, in its summarize stage, already reschedules itself whenever pending batches remain:

const pending = query.runSuiteQL({
    query: "SELECT COUNT(id) cnt FROM customrecord_batch " +
           "WHERE status = 'PENDING'"
}).asMappedResults();

if (Number(pending[0].cnt) > 0) {
    task.create({ taskType: task.TaskType.MAP_REDUCE,
                  scriptId: MR_SCRIPT, deploymentId: MR_DEPLOY }).submit();
}

So the second user's batches sit at PENDING, the running instance finishes, its summarize sees pending work and starts another pass. The work happens. On its own. Without anyone doing anything.

MAP_REDUCE_ALREADY_RUNNING is not reporting a failed action. It is reporting the state of the scheduler — and the scheduler being busy is precisely the condition the reschedule logic exists to handle.

The distinction to hold onto: "did the user's work get recorded" and "did processing start immediately" are two different questions. Conflating them is what turns a queue into a duplicate-processing bug.

The fix

Catch that specific error, treat it as a successful queue, and say so:

let queued = false;
try {
    task.create({ /* ... */ }).submit();
} catch (taskErr) {
    if (taskErr.name === 'MAP_REDUCE_ALREADY_RUNNING') {
        queued = true;
        log.audit(fn, batchIds.length + ' batch(es) left PENDING for summarize() to pick up.');
    } else {
        throw taskErr;                       // anything else is real
    }
}

return { success: true, queued: queued, orderCount: soIds.length };

And the message the user sees becomes the difference between a good afternoon and a bad one:

"48 orders queued — they'll start when the current batch finishes."

Note the throw in the else branch. Swallowing every exception from submit() would hide a genuine scheduling failure and leave batches pending forever with nobody told. Only the one named condition is benign.

The same shape, elsewhere

Once you have the pattern you start seeing it. In the same Suitelet, deleting a batch:

try {
    record.delete({ type: BATCH_REC, id: Number(id) });
    deleted++;
} catch (e) {
    /* Already gone - stale tab, double click, someone else deleted it.
       Same outcome the user wanted. */
    if (e.name === 'RCRD_DSNT_EXIST') { deleted++; }
    else { log.error(fn + 'id=' + id, e); }
}

Somebody clicks Delete on a batch that a colleague removed two minutes ago. The record does not exist. That is an error in the strict sense and a non-event in every sense that matters — the user wanted it gone, and it is gone. Reporting a failure teaches people the delete button is unreliable.

Both are the same family: an exception telling you the world is already in the state you were trying to produce.

Design queue-first and this gets easier

The reason the fix is three lines rather than a rewrite is that the records were written before the task was submitted. That ordering is worth adopting deliberately:

  1. Write the intent as records, in a pending state.
  2. Try to start processing.
  3. Have the processor pick up anything pending when it finishes.

With that shape, "could not start right now" is genuinely not an error — the work is durable and something will collect it. Governance limits, concurrency caps, a queue that is momentarily full: all of them become delays rather than failures.

The inverse shape — try to start, and only record the work if starting succeeded — makes every transient scheduling condition into lost work, and every user response to it into a duplicate.

What to take from it

When a platform throws, ask what it is describing. A lot of exceptions describe the environment rather than the outcome of the request: something is busy, something already exists, something is already absent. Those are not the user's problem and should not be shown to them as failures.

The test is simple. If the honest answer to "what should the user do differently?" is "nothing", it should not be an error message.

Related: is the error you’re chasing the cause, or just a symptom?

New England Systems Group

If there's a process everyone works around instead of through, that's where we start.

SuiteScript development and back-office automation for manufacturers and distributors. Danbury, Connecticut.

hello@nesystemsgroup.com