Promise.all, integrating errors with apex.message, and aborting in-flight requests

.

Every APEX developer reaches for apex.server.process sooner or later. It is how JavaScript on the page talks to PL/SQL on the server: fetch some data, run a validation, kick off a background job. The basic call is easy. What I find trips people up is everything around it once the page gets real. How do you run three calls at once instead of waiting for each in turn? How do errors reach the user without an unfriendly alert() box? What happens to a search request when the user types another character before the first one comes back? This post covers those three: parallel calls with Promise.all, surfacing errors through apex.message, and aborting requests that are already in flight.

Parallel calls with Promise.all

The pattern I find most under-used in existing APEX code is running multiple server processes in parallel. The sequential version:

async function loaddashboard() {
   const orders = await apex.server.process('GET_ORDERS', { dataType: 'json' });
   const products = await apex.server.process('GET_TOP_PRODUCTS', { dataType: 'json' });
   const customers = await apex.server.process('GET_NEW_CUSTOMERS', { dataType: 'json' });

   renderdashboard(orders, products, customers);
}

Three round trips to the server, each waiting for the previous one to finish. If each takes 200ms, the dashboard takes 600ms to load.

The same code with Promise.all runs all three in parallel:

async function loaddashboard() {
   try {
      const [orders, products, customers] = await Promise.all([
         apex.server.process('GET_ORDERS', {}, { dataType: 'json' }),
         apex.server.process('GET_TOP_PRODUCTS', {}, { dataType: 'json' }),
         apex.server.process('GET_NEW_CUSTOMERS', {}, { dataType: 'json' })
      ]);

      renderdashboard(orders, products, customers);
   } catch (err) {
      apex.message.showErrors([{
         type: 'error',
         location: 'page',
         message: 'Some dashboard data could not load.',
         unsafe: false
      }]);
      console.error('loaddashboard failed:', err);
   }
}

Now the total time is whatever the slowest call takes. On a dashboard with three independent data fetches, this is often the difference between "feels responsive" and "feels slow."

Promise.all has one important property: if any of the promises reject, the whole thing rejects immediately, and the other in-flight calls keep running but their results are discarded. If you want partial success behaviour (render what loaded, show errors for what failed), use Promise.allSettled instead:

const results = await Promise.allSettled([
   apex.server.process('GET_ORDERS', {}, { dataType: 'json' }),
   apex.server.process('GET_TOP_PRODUCTS', {}, { dataType: 'json' }),
   apex.server.process('GET_NEW_CUSTOMERS', {}, { dataType: 'json' })
]);

results.forEach((r, i) => {
   if (r.status === 'fulfilled') {
      // render the successful one
   } else {
      // log or display the rejection
   }
});

I default to Promise.all when the page genuinely needs all three results, and Promise.allSettled when partial rendering is acceptable. The choice depends on how the page degrades when one call fails.

Sequencing dependent calls

Promise.all is for independent calls. If one call depends on the result of another, sequential await is the right pattern:

async function placeorder() {
   const customer = await apex.server.process('VALIDATE_CUSTOMER', { x01: $v('P20_CUSTOMER_ID') }, { dataType: 'json' });

   if (customer.status !== 'active') {
      apex.message.showErrors([{
         type: 'error',
         location: 'page',
         message: 'Customer is not active. Cannot place order.',
         unsafe: false
      }]);
      return;
   }

   const order = await apex.server.process('CREATE_ORDER', {
      x01: customer.id,
      x02: $v('P20_PRODUCT_ID'),
      x03: $v('P20_QUANTITY')
   }, { dataType: 'json' });

   $s('P20_ORDER_ID', order.id);
   apex.message.showPageSuccess('Order created: ' + order.reference);
}

The validate call has to complete before the create call has the customer ID to work with. Each await blocks the next line until the server responds. This is the right shape when the data flow is genuinely sequential.

What you want to avoid is the accidental sequential pattern: two await calls in a row that don't actually depend on each other. That is Promise.all territory.

Aborting in-flight requests

This is a feature most APEX developers don't realise they have. The promise returned by apex.server.process has an .abort() method, documented in the Oracle API reference. Useful for type-ahead search, dependent select lists, anything where a new request invalidates an in-flight one.

let currentrequest = null;

async function searchproducts(searchterm) {
   if (currentrequest) {
      currentrequest.abort();
   }

   currentrequest = apex.server.process('SEARCH_PRODUCTS', { x01: searchterm }, { dataType: 'json' });

   try {
      const results = await currentrequest;
      renderresults(results);
   } catch (err) {
      // ignore abort errors, but log real failures
      if (err && err.statusText !== 'abort') {
         console.error('searchproducts failed:', err);
      }
   }
}

Two things to watch for. First, the abort method documented in the APEX docs notes that abort does not work for requests that use any queue options. If you have configured pOptions with a queue setting, you cannot cancel mid-flight. Second, an aborted request rejects the promise with an error whose statusText is 'abort'. You probably want to filter that out of your error display, since the user did not encounter a failure, you cancelled the call on their behalf.

A note on the queue option

The queue option in apex.server.process is worth a paragraph because it changes the async story. By default, every call is independent and runs in parallel with anything else in flight. If you specify a queue name, calls in the same queue run in sequence and can be configured to abort previous calls in the queue.

apex.server.process('SEARCH_PRODUCTS', { x01: term }, {
   dataType: 'json',
   queue: { name: 'search', action: 'replace' }
});

With action: 'replace', a new call in the search queue aborts any pending call in that queue. This is built-in debouncing for the common case of type-ahead inputs, and it removes the need for the manual abort pattern above. The trade-off is that the returned promise's .abort() method does not work when queue options are in use.

I tend to use the manual abort pattern when I need fine-grained control, and the queue option when the behaviour I want is "always abort the previous in-flight call." Both are valid choices.

Summary

The short version of this post is:

  1. Use Promise.all for parallel independent calls. Use sequential await only when calls depend on each other.

  2. The promise has an .abort() method. Use it for type-ahead and similar patterns, or use the queue option for the same effect.

Previous
Previous

Build Options in APEX: Five Patterns to Keep In Mind

Next
Next

Understanding apex.server.process and async/await