🔹 Quick Primer — What async/await means in SuiteScript
- SuiteScript 2.1 supports modern JavaScript syntax, including
async/await
. - Many server-side NetSuite APIs are synchronous (e.g.,
record.load
,search.run
,https.request
).await
won’t make those faster or non-blocking. - Client-side (browser) code can benefit from true async (e.g.,
fetch
,N/ui/dialog
promises, waiting for user input, polling a Suitelet/RESTlet). - Use
async/await
mainly for:- Prompting users and continuing after they click (Client Script).
- Calling Suitelet/RESTlet endpoints from the browser and updating the UI without a full page reload.
- Polling Map/Reduce task status (sleep + retry).
- Retrying flaky network operations with backoff.
- Coordinating multi-step flows cleanly with
try/catch
.
Important:
async/await
does not bypass governance. It just makes async flows easier to read/maintain.
🔹 Pattern 1 — User Prompts (Client) with await
N/ui/dialog
methods return Promises in the browser. Combine with try/catch
for clean control flow.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* Pattern: Await user confirmation before save
*/
define(['N/ui/dialog', 'N/currentRecord', 'N/log'], (dialog, currentRecord, log) => {
const saveRecord = async () => {
try {
const ok = await dialog.confirm({
title: 'Confirm',
message: 'Do you want to submit this record now?'
});
if (!ok) return false; // cancel save
// Optional: another async step
await dialog.alert({ title: 'Info', message: 'Submitting…' });
return true; // allow save
} catch (e) {
log.error({ title: 'Confirm Error', details: e });
return false;
}
};
return { saveRecord };
});
Why it’s good: no nested callbacks; the flow reads top-to-bottom.
🔹 Pattern 2 — Suitelet + Client “fetch” (AJAX) with await
Use a Suitelet as a JSON endpoint and call it from the client to fetch data or perform lightweight actions without reloading the page.
Client Script (browser)
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* Pattern: Fetch data from Suitelet endpoint and render
*/
define(['N/url', 'N/log'], (url, log) => {
const fetchData = async () => {
try {
const suiteletUrl = url.resolveScript({
scriptId: 'customscript_my_json_sl',
deploymentId: 'customdeploy_my_json_sl',
params: { action: 'summary' }
});
const res = await fetch(suiteletUrl, { method: 'GET', credentials: 'same-origin' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
// Update page elements (browser DOM)
document.getElementById('summaryBox').textContent = json.summaryText;
} catch (e) {
log.error({ title: 'Fetch Error', details: e });
alert('Unable to load summary. Try again.');
}
};
// expose for button onclick
return { fetchData };
});
Suitelet (server)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* Returns JSON when action=summary
*/
define(['N/search'], (search) => {
const onRequest = (ctx) => {
if (ctx.request.method === 'GET' && ctx.request.parameters.action === 'summary') {
const count = search.create({ type: 'salesorder', filters: [], columns: [] })
.runPaged({ pageSize: 1 }).count;
ctx.response.addHeader({ name: 'Content-Type', value: 'application/json' });
ctx.response.write(JSON.stringify({ summaryText: `Open Sales Orders: ${count}` }));
return;
}
ctx.response.write('OK');
};
return { onRequest };
});
Why it’s good: smooth UX, no page reloads, clear await
error handling.
🔹 Pattern 3 — Start Map/Reduce then poll status with await sleep()
Start a background job (Map/Reduce, Scheduled) and poll its status from a Suitelet or Client Script. await
makes the polling loop straightforward.
// helper sleep for both client & server contexts that support setTimeout
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
Suitelet that starts MR and waits (server)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* Pattern: Start MR then poll status before responding
*/
define(['N/task','N/log'], (task, log) => {
const POLL_MS = 1500;
const MAX_TRIES = 40;
const onRequest = async (ctx) => {
try {
const t = task.create({ taskType: task.TaskType.MAP_REDUCE, scriptId: 'customscript_my_mr', deploymentId: 'customdeploy_my_mr' });
const taskId = t.submit();
let tries = 0, status;
while (tries++ < MAX_TRIES) {
status = task.checkStatus(taskId);
if (status.status === task.TaskStatus.COMPLETE || status.status === task.TaskStatus.FAILED) break;
await sleep(POLL_MS);
}
ctx.response.write(`Task ${taskId} finished with status: ${status.status}`);
} catch (e) {
log.error('Suitelet Poll Error', e);
ctx.response.write(`Error: ${e.message}`);
}
};
return { onRequest };
});
Server scripts don’t run “truly async” in parallel here—the
await sleep
just makes the polling loop simpler and keeps your code readable. For long jobs, prefer responding immediately and letting the client poll a status endpoint instead (see next pattern).
🔹 Pattern 4 — Client-side status polling (non-blocking UI)
Let the client do the waiting so your Suitelet responds fast.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* Pattern: Start MR (via Suitelet) then poll a status endpoint
*/
define(['N/url'], (url) => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const runBatch = async () => {
// 1) Start job
const startUrl = url.resolveScript({ scriptId:'customscript_start_mr_sl', deploymentId:'customdeploy_start_mr_sl' });
const startRes = await fetch(startUrl, { method:'POST', credentials: 'same-origin' });
const { taskId } = await startRes.json();
// 2) Poll status endpoint
const statusUrl = url.resolveScript({ scriptId:'customscript_status_sl', deploymentId:'customdeploy_status_sl', params:{ taskId }});
let done = false, tries = 0;
while (!done && tries++ < 120) { // ~2 min
const res = await fetch(statusUrl, { credentials:'same-origin' });
const { status } = await res.json();
document.getElementById('statusBox').textContent = status;
done = (status === 'COMPLETE' || status === 'FAILED');
if (!done) await sleep(1000);
}
};
return { runBatch };
});
Why it’s good: user sees progress; server endpoints stay short-lived.
🔹 Pattern 5 — Retry with Exponential Backoff for flaky calls
Great for intermittent network hiccups (client calling Suitelet/RESTlet or external APIs from a Suitelet).
async function withBackoff(fn, { tries = 5, baseMs = 300 } = {}) {
let attempt = 0, lastErr;
while (attempt < tries) {
try { return await fn(); }
catch (e) {
lastErr = e;
const delay = baseMs * Math.pow(2, attempt); // 300, 600, 1200, ...
await new Promise(r => setTimeout(r, delay));
attempt++;
}
}
throw lastErr;
}
// usage in client code
const data = await withBackoff(() => fetchJson('/app/site/hosting/scriptlet.nl?script=...'));
🔹 Pattern 6 — Parallel requests with Promise.allSettled
(Client)
When you must hit several lightweight endpoints, do them in parallel and handle partial failures gracefully.
const endpoints = [u1, u2, u3];
const results = await Promise.allSettled(endpoints.map(u => fetch(u, { credentials:'same-origin' }).then(r => r.json())));
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const fail = results.filter(r => r.status === 'rejected');
Tip: Keep batch sizes small to avoid rate limits or session issues.
🔹 When NOT to use async/await
- Inside heavy Map/Reduce logic for standard NetSuite calls (record/search): those APIs are synchronous—
await
adds no benefit. - To “speed up” synchronous calls (e.g.,
record.save
)—you can’t. - To bypass governance—you can’t.
🔹 Robust Error Handling Template
Use try/catch/finally
and return meaningful messages to the UI.
const doAction = async () => {
try {
// … your awaited steps …
return { ok: true };
} catch (e) {
// log server-side; surface safe message client-side
return { ok: false, message: e.message || 'Unexpected error' };
} finally {
// optional cleanup
}
};
🔹 Practical Use-Case Ideas
- Advanced Suitelet “Run & Watch”: user clicks “Process,” Suitelet starts MR and immediately returns a taskId; Client polls status and streams a small log area.
- Inline Validations in forms:
await dialog.confirm()
/await dialog.alert()
before save. - On-page Dashboards: Client periodically
await fetch(...)
to pull KPIs from a Suitelet JSON endpoint (avoid re-rendering the whole page).
âś… Summary
- SuiteScript 2.1 gives you
async/await
, but most server APIs are still synchronous. - Use
async/await
where it shines:- Client-side UX (prompts, fetch to Suitelet/RESTlet).
- Polling & retries for long/remote operations.
- Parallelism for multiple lightweight client requests.
- Keep heavy work in Map/Reduce, and keep Suitelet endpoints fast.
async/await
= cleaner code + safer flows—not a shortcut around governance.