Understanding apex.server.process and async/await

.

If you write JavaScript in Oracle APEX, you almost certainly use apex.server.process to call PL/SQL from the browser. It is the bread and butter of any non-trivial APEX page: fetching data on demand, validating before submit, triggering server-side work without a full page reload. This blog covers understanding what apex.server.process actually returns and how that interacts with await.

What apex.server.process actually returns

The APEX JavaScript API documentation says:

A promise object is returned. The promise done method is called if the Ajax request completes successfully... The promise also has an always method that is called after done and error.

"Promise" here is doing a lot of work. What apex.server.process actually returns is a jQuery deferred, which has .done(), .fail(), and .always() methods rather than the standard .then() and .catch(). jQuery deferreds are thenable (an object or function that defines a then method), meaning they conform enough to the Promise A+ spec (refers to the extension of the Promises/A spec.) that await can work with them, but their behaviour differs from a native Promise in two important ways.

First, the rejection callback signature is different. A native Promise's .catch(err) gives you a single error argument. A jQuery deferred's .fail(jqXHR, textStatus, errorThrown) gives you three. When you await a deferred and it rejects, the throw is constructed from the deferred's rejection arguments and the result is not the clean Error object you might expect.

Second, jQuery deferreds historically swallowed errors thrown inside .done() handlers, though modern jQuery (3.x and later) has fixed this. APEX has shipped recent jQuery, so this is mostly a historical concern.

Now to the practical question. Do you actually need to wrap it?

A common pattern is:

function getproducts(categoryid) {
   return new Promise((resolve, reject) => {
      apex.server.process(
         'GET_PRODUCTS',
         { x01: categoryid },
         {
            dataType: 'json',
            success: resolve,
            error: reject
         }
      );
   });
}

This converts the jQuery deferred into a native Promise with standard .then()/.catch() semantics. It works. But the simpler version also works in APEX:

async function getproducts(categoryid) {
   return apex.server.process(
      'GET_PRODUCTS',
      { x01: categoryid },
      { dataType: 'json' }
   );
}

The async keyword wraps whatever the function returns into a Promise, and await on the returned thenable will resolve to the JSON response. No manual wrapping needed.

When the simpler version is fine:

  • You only care about the success value.

  • You handle errors with try/catch around the await call.

  • You don't need to inspect the jQuery-specific error arguments (jqXHR, textStatus, errorThrown).

When you should wrap it explicitly:

  • You need to attach jQuery-specific success/error logic via the options object and return a clean Promise to callers.

  • You need to inspect the full rejection trio to distinguish, say, a timeout from a server-side error.

  • You're building a library function that other developers will consume and you want predictable Promise semantics.

For most page-level JavaScript in APEX applications, the simpler version is what I reach for first.

The error handling story

The most common bug I see in async/await code that calls apex.server.process is missing error handling. The below displays one of the patterns that seems to work seamlessly:

async function loadcategorycount() {
   try {
      const result = await apex.server.process(
         'GET_CATEGORY_COUNT',
         { x01: $v('P10_CATEGORY_ID') },
         { dataType: 'json' }
      );

      $s('P10_COUNT', result.count);
   } catch (err) {
      apex.message.showErrors([{
         type: 'error',
         location: 'page',
         message: 'Could not load category count.',
         unsafe: false
      }]);
      console.error('getcategorycount failed:', err);
   }
}

A few things worth noting:

Always wrap the await in try/catch.

An unhandled rejection in an async function becomes an unhandled Promise rejection at the global level. In the browser, that surfaces as a console warning and silent failure. The user sees nothing.

Use apex.message.showErrors rather than alert or console.log alone.

The apex.message API renders errors in the standard APEX notification region, which means they look like every other validation error the user sees and obey the same dismissal rules. The location property accepts 'page' for the top notification region or 'inline' for a specific page item.

Log the underlying error to the console.

Whatever you show the user should be friendly. The actual err object often contains useful diagnostic information that you want available when someone reports a bug.

One thing to be cautious about: if the PL/SQL on-demand process raises an exception, what apex.server.process returns in the error path depends on how the error is surfaced.

A RAISE_APPLICATION_ERROR in your on-demand process produces an HTTP 500 response that the deferred treats as a failure. A PL/SQL block that completes successfully but returns an error structure in its JSON payload is a success from the AJAX layer's point of view. You need to handle the second case yourself:

const result = await apex.server.process('SAVE_ORDER', { ... });

if (result.status === 'error') {
   apex.message.showErrors([{
      type: 'error',
      location: 'page',
      message: result.message,
      unsafe: false
   }]);
   return;
}

// proceed with success path

I prefer returning structured error responses from on-demand processes rather than raising exceptions, because it gives the JavaScript side cleaner control over the user experience. But that is an opinion, not a rule.

Summary

The short version of this post is:

  1. apex.server.process returns a jQuery deferred, which is thenable. You can await it directly in APEX without wrapping it in a new Promise.

  2. Always wrap the await in try/catch and surface errors through apex.message.showErrors.

Previous
Previous

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

Next
Next

Bulk Collect and FORALL: Correctness, Sparse Collections, and Knowing When Not To - Part II