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 of2
- If
promise1
succeeds, we display the data. If it fails we display the error (displayError
). - Then we invoke
promise2
. We use the data frompromise1
and pass it topromise2
. - 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
andpromise2
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
andpromise2
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
andPromise.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...!