JavaScript Promises in APEX

JavaScript Promises in APEX

As I was reading the APEX 5.1 API documentation the other day, I noticed something pretty interesting under the apex.server namespace.

apex.server.process and apex.server.plugin are now returning JavaScript Promises.

apex.server.plugin( pAjaxIdentifier, pData, pOptions ) → Promise
apex.server.process( pName, pData, pOptions ) → Promise

What are JavaScript Promises?

A JavaScript Promise is essentially an object that returns asynchronous code.

Let's take a step back for a second...

Synchronous vs asynchronous

Synchronous coding always runs the code from top to bottom.

  • Pros: It's easy and logical to read from the human eye.
  • Cons: It's slow and it's blocking your user's interactions when the code is running.

Asynchronous coding allows the code to continue to run when longer processes are launched.

  • Pros: It's much faster and non-blocking.
  • Cons: It inverses the order in which a human reads the code.

Just remember this: Synchronous is blocking. Asynchronous is non-blocking. Asynchronous is good. Synchronous is bad.

Using Callbacks

Asynchronous code is achieved by using callbacks. Let's have a look at the following code:

console.log(1);

apex.server.process(
    "some_ajax", {}, {
        success: function(data) {
            console.log(2);
        }
    }
);

console.log(3);

Reading from top to bottom in a synchronous way, we would expect the outcome to be:

1
2
3

But apex.server.process is not synchronous, so while it does whatever's in "some_ajax", the code continues to run, resulting in the following outcome:

1
3
2

Yeah... So let's just continue to nest the code inside the callbacks.

apex.server.process(
    "ajax1", {}, {
        success: function(data) {
            console.log(1);
            apex.server.process(
                "ajax2", {}, {
                    success: function(data) {
                        console.log(2);
                        apex.server.process(
                            "ajax3", {}, {
                                success: function(data) {
                                    console.log(3);
                                }
                            }
                        );
                    }
                }
            );
        }
    }
);

That my friends, is what we call "Callback Hell"...

Unravelling the Callbacks

With Promises, we get the best of both worlds by benefiting from the fast and non-blocking asynchronous code, but by writing it in a readable top-to-bottom way.

Let's follow the demo below.

Setup

A demo is available here, directly on the login page. Make sure to open up your browser's console to see what's going on.

APEX - Ajax Callbacks

We will be creating 3 Ajax Callbacks.

ajax1
Simulates a 1 second process and returns a JSON object.

declare
    l_now timestamp := systimestamp;
    l_end_time timestamp;
begin
    l_end_time := l_now + numtodsinterval (1, 'second');

    while(l_end_time > l_now) loop
        l_now := systimestamp;
    end loop;

    apex_json.open_object;
    apex_json.write('success', true);
    apex_json.write('x01', apex_application.g_x01);
    apex_json.close_object;
exception
    when others then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', sqlerrm);
        apex_json.close_object;
end;

ajax2
Simulates a 2 seconds process and returns a JSON object.

declare
    l_now timestamp := systimestamp;
    l_end_time timestamp;
begin
    l_end_time := l_now + numtodsinterval (2, 'second');

    while(l_end_time > l_now) loop
        l_now := systimestamp;
    end loop;

    apex_json.open_object;
    apex_json.write('success', true);
    apex_json.write('x01', apex_application.g_x01);
    apex_json.close_object;
exception
    when others then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', sqlerrm);
        apex_json.close_object;
end;

ajax_error
Simulates a failed process by raising an error.

raise_application_error(-20001, sqlerrm);

APEX - Page Properties

The code below is added to the page properties, under the Function and Global Variable Declaration section.

// Logs an error when a Promise is rejected
var displayError = function(error) {
    console.error("Promise is rejected:", error);
};

// Function returning a Promise (ajax1)
var promise1 = function(param) {
    return apex.server.process(
        "ajax1", {
            x01: param
        }
    );
};

// Function returning a Promise (ajax2)
var promise2 = function(param) {
    return apex.server.process(
        "ajax2", {
            x01: param
        }
    );
};

// Function returning a Promise (ajax_error)
var promiseError = function() {
    return apex.server.process(
        "ajax_error", {}
    );
};

Creating a Promise

As shown above, we'll encapsulate all apex.server.process inside functions and we'll simply return the object. As I mentioned at the beginning, apex.server.process returns a Promise, so our functions will be returning Promises as well.

Statuses

A Promise has 3 possible statuses:

  • Pending: When a Promise is invoked, it's set to Pending. The data hasn't returned yet.
  • Resolved: A Promise is Resolved when the data came back successfully.
  • Rejected: A Promise is Rejected when the data came back with errors.

Once a Promise is resolved or rejected, it can never change to another status again.

How to use a Promise

then

The code below is added to the page properties, under the Execute when Page Loads section.

// Chaining the promises
// Simulating a synchronous process chain
promise1(2)
    .then(function(data) {
        console.log("promise1 is resolved:", data);
        // passing the data from promise1 to promise2
        // and doubling the value of x01
        return promise2(data.x01 * 2);
    }, displayError)
    .then(function(data) {
        console.log("promise2 is resolved:", data);
        return promiseError();
    }, displayError)
    .then(function(data) {
        console.log("promiseError is resolved (Never gonna happen...):", data);
    }, displayError);

What happens here?

  • We invoke promise1 with a parameter of 2
  • If promise1 succeeds, we display the data. If it fails we display the error (displayError).
  • Then we invoke promise2. We use the data from promise1 and pass it to promise2.
  • If promise2 succeeds, we display the data. If it fails we display the error (displayError).
  • Then we invoke promiseError.
  • promiseError will never succeed, so we display the the error (displayError).

all

The code below is added to the page properties, under the Execute when Page Loads section.

// Executing all the promises at the same time
// Waiting for all of them to return
Promise.all([promise1(2), promise2(4)])
    .then(function(data) {
        console.log("All Promises are resolved:", data);
    }).catch(displayError);

What happens here?

  • We invoke promise1 and promise2 at the same time.
  • It waits until all Promises have returned.
  • If all Promises succeed, we display all the data (as an array of objects).
  • If one or more Promise fails, we display the error (displayError).

race

The code below is added to the page properties, under the Execute when Page Loads section.

// Executing all the promises at the same time
// Waiting for the **first** one to return
Promise.race([promise1(2), promise2(4)])
    .then(function(data) {
        console.log("Fastest Promise is resolved:", data);
    }).catch(displayError);

What happens here?

  • We invoke promise1 and promise2 at the same time.
  • It waits until the first Promise has returned.
  • If that first Promise succeeds, we display it's data.
  • If that first Promise fails, we display the error (displayError).

catch

Notice the .catch at the bottom of the previous example. That is simply catch the error that occurs during one of more promise. Just like a regular try {} catch {} would do.

Final Result

When all the code above is launched on page load, here's what we get:

What happens here?

  • The 3 code blocks launched simultaneously* (promise1, Promise.all and Promise.race).
  • promise1 and Promise.race returned simultaneously after 1 second.
  • promise2 launched.
  • Promise.all returned after 2 seconds.
  • promise2 returned after 3 seconds (1 second for promise1 and 2 seconds for promise2).
  • promiseError launched.
  • promiseError failed.

*Almost simultaneously...


Perhaps not a surprise, but this whole snippet of code also works on APEX 5.0, because 5.0's apex.server.process is returning a jqXHR object, which implements the Promise interface...

Damn, I wish I knew this earlier...!