SuiteScript 2.1 has async support on the client side. Most NetSuite developers aren't using it. That means users sitting through loading spinners while records load one by one, when they could all load at the same time.
I benchmarked this on a production NetSuite account. The difference is not subtle.
The problem: sequential record loading
Here's what most SuiteScript looks like when you need to load multiple records:
const RECORD_TYPE = 'salesorder';
const RECORD_IDS = [226296, 226188, 226088, 225984, 225983];
const results = [];
for (const id of RECORD_IDS) {
const rec = record.load({ type: RECORD_TYPE, id });
results.push({
id,
tranid: rec.getValue('tranid'),
total: rec.getValue('total'),
status: rec.getText('status'),
});
}
Every record.load() call is an HTTP round-trip from the browser to NetSuite's server and back. Five records means five sequential round-trips. The browser sits idle between each one, waiting for the response before firing the next.
ASYNC vs SYNC Demo
[SYNC] Sequential...
[SYNC] Done in 5723ms
┌─────────┬────────┬────────────┬────────┬─────────────────────────┐
│ (index) │ id │ tranid │ total │ status │
├─────────┼────────┼────────────┼────────┼─────────────────────────┤
│ 0 │ 226296 │ '13175' │ 155.95 │ 'Billed' │
│ 1 │ 226188 │ '13174' │ 415.95 │ 'Billed' │
│ 2 │ 226088 │ '13173' │ 315.95 │ 'Billed' │
│ 3 │ 225984 │ '13172' │ 415.95 │ 'Billed' │
│ 4 │ 225983 │ 'SO102038' │ 3537.4 │ 'Pending Fulfillment' │
└─────────┴────────┴────────────┴────────┴─────────────────────────┘
5,723ms. Almost 6 seconds for 5 records. Your user is waiting that entire time.
Quick primer: what is a Promise?
If you already know how Promises work, skip ahead. If not, here is the short version.
A Promise is a JavaScript object that represents a value you don't have yet. When you call a function that returns a Promise, the work starts immediately, but your code doesn't stop and wait for it. It keeps going to the next line.
Promise.all takes an array of Promises and waits for all of them to finish. Because each request started at roughly the same time, the total wait equals the slowest single request instead of the sum of all of them.
Think of it like a restaurant. The synchronous approach is ordering one dish, waiting for it to arrive, then ordering the next. Promise.all is ordering everything at once and having the kitchen work on all of it at the same time.
The fix: parallel loading with Promise.all
SuiteScript 2.1 exposes .promise() variants on most client-side API calls. Instead of record.load(), call record.load.promise(). It returns a Promise. Wrap them in Promise.all and they fire simultaneously:
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/record'], (record) => {
const pageInit = async (context) => {
const RECORD_TYPE = 'salesorder';
const RECORD_IDS = [226296, 226188, 226088, 225984, 225983];
const results = await Promise.all(
RECORD_IDS.map(id =>
record.load.promise({ type: RECORD_TYPE, id })
.then(rec => ({
id,
tranid: rec.getValue('tranid'),
total: rec.getValue('total'),
status: rec.getText('status'),
}))
)
);
console.table(results);
};
return { pageInit };
});
Same data. Same results:
[ASYNC] Parallel...
[ASYNC] Done in 1285ms
┌─────────┬────────┬────────────┬────────┬─────────────────────────┐
│ (index) │ id │ tranid │ total │ status │
├─────────┼────────┼────────────┼────────┼─────────────────────────┤
│ 0 │ 226296 │ '13175' │ 155.95 │ 'Billed' │
│ 1 │ 226188 │ '13174' │ 415.95 │ 'Billed' │
│ 2 │ 226088 │ '13173' │ 315.95 │ 'Billed' │
│ 3 │ 225984 │ '13172' │ 415.95 │ 'Billed' │
│ 4 │ 225983 │ 'SO102038' │ 3537.4 │ 'Pending Fulfillment' │
└─────────┴────────┴────────────┴────────┴─────────────────────────┘
1,285ms. 4.4x faster. Almost 6 seconds down to 1.3 seconds. Same records, completely different user experience.
Why this works
The sync version sends one HTTP request, waits for the response, then sends the next. Each round-trip takes about 1,100ms. Five records means five round-trips stacked end to end.
The async version fires all five requests at once. The browser's JavaScript engine (V8 in Chrome, SpiderMonkey in Firefox) has a real event loop that handles multiple concurrent HTTP connections natively. Promise.all resolves when the slowest one comes back, not when all of them have been processed sequentially.
This is real parallelism. The browser is making five simultaneous network calls.
Which client-side modules support .promise()?
Most of the modules you already use have .promise() variants. Here is the full list from NetSuite's documentation:
- N/record -
load,create,copy,delete,transform,attach,detach,submitFields,Record.save,Record.executeMacro,Macro.execute,Macro.promise - N/search -
create,load,delete,lookupFields,global,duplicates,Search.runPaged,Search.save,ResultSet.getRange,ResultSet.each,PagedData.fetch,Page.next,Page.prev - N/query -
load,Query.run,Query.runPaged - N/https -
get,post,put,request,requestSuitelet - N/http -
get,post,put,delete,request - N/email -
send,sendBulk,sendCampaignEvent - N/action -
execute,find,get,Action.promise,Action.execute - N/currentRecord -
get - N/dataset -
describe,loadDataset,Dataset.run - N/transaction -
void
Append .promise() to any of these. If you are calling it from a client script, the async variant exists.
When to use this
Any time your client script fires off multiple independent requests, they should run in parallel:
- pageInit handlers that populate fields from related records. Loading customer data, pricing data, and inventory data at the same time instead of one after another.
- fieldChanged handlers that validate against multiple entities. Credit check, pricing lookup, and stock check all at once.
- Custom buttons that pull data before performing an action.
- Suitelet calls via
https.requestSuitelet.promise()when you need data from multiple backend endpoints.
The rule is simple: if request B doesn't depend on the result of request A, don't wait for A to finish before starting B.
Error handling with Promise.allSettled
One thing to watch: Promise.all rejects the entire batch if any single request fails. If you need partial results, use Promise.allSettled instead:
const settled = await Promise.allSettled(
RECORD_IDS.map(id =>
record.load.promise({ type: RECORD_TYPE, id })
)
);
// Separate successes from failures
const loaded = settled
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = settled
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (failed.length) {
log.error('Failed loads', JSON.stringify(failed));
}
Use Promise.all when every request must succeed for the operation to make sense. Use Promise.allSettled when you want to handle failures gracefully and still process whatever came back successfully.
A note on server-side async
SuiteScript 2.1 also exposes .promise() on the server for a subset of modules: N/search, N/query, N/http, N/https, N/transaction, and N/llm. You might see benchmarks (including some I originally published) showing 2-4x speedups on the server side.
After extensive testing, I found that story is more complicated than it first appears. Server-side .promise() does not behave the same way as client-side. I wrote a full investigation with forensic benchmarks and execution logs in a separate post: What .promise() Actually Does on the Server.
The short version: client-side async is where the real performance wins are. That is the focus of this post, and that is where you should start.
Need help optimizing your SuiteScript?
I write production SuiteScript for businesses that need NetSuite to be fast and reliable. If your scripts are slow or your customizations need a rewrite, let's talk.