Coding With Promises in Profound.js
All Profound.js framework APIs that perform asynchronous operations (such as for reading files, DB queries, etc.) return a Promise. Profound.js programs are intended to run top-down using async/await and must wait for any asynchronous operations to complete before returning. To make coding easier, and to avoid the pitfalls mentioned in Coding With Promises in Node.js, the Profound.js framework automatically adds async/await where needed to any asynchronous Profound.js framework API calls at module transformation time.
What is Module Transformation?
When Profound.js first loads a program into memory, it performs some automatic changes to the code behind the scenes. The changes are done only in memory – they are not written back to the program’s source code file. These changes are called module transformation. You can see the result of module transformation by debugging a Profound.js program – the running code shown in the debugger will look a bit different than your source code.
How Does Module Transformation Help With Promises?
For example, consider a program like this:
function app() {
pjs.defineDisplay("display", "app.json");
loadGrid();
display.screen.execute();
function loadGrid() {
let products = getProducts();
display.grid.replaceRecords(products);
}
function getProducts() {
return pjs.query("select * from productsp");
}
}
exports.default = app;
The example uses a few asynchronous Profound.js APIs to define a display, query a database, and to show the screen. The code will run in the correct sequence without having to manually add async/await, because module transformation takes care of it. Behind the scenes, the code is transformed into something like this:
async function app() {
await pjs.defineDisplay("display", "app.json");
await loadGrid();
await display.screen.execute();
async function loadGrid() {
let products = await getProducts();
display.grid.replaceRecords(products);
}
async function getProducts() {
return await pjs.query("select * from productsp");
}
}
exports.default = app;
Module transformation for Promises works like this:
All top-level named functions and first-level inner named functions in a Profound.js module are changed to async, if not already defined that way.
await is added onto any Profound.js framework API calls, if not already coded.
Calls to require() and pjs.import() are detected, and the target module loaded/transformed if possible. Calls to any imported async functions have await added if not already coded.
If module transformation adds await inside a function that appears to be used with an Array iteration method (i.e. Array.forEach()), the Array iteration method is optionally replaced with a Promises-aware version that runs the callback for each array element in series. await is added on the call to the Array iteration method. See asyncArrayIteration.
Any function that module transformation inserts an await into is changed to async if not already coded that way.
Then, any calls to functions that are found to be async as the result of the above have await added, if necessary, and the process repeats over the entire call tree.
Optionally, await can be added to calls to all async functions in the module. See autoAsyncAwait.
Using Asynchronous Node.js and 3rd Party APIs in Profound.js
A Profound.js application is expected to run top-down and the framework expects that there are no Promises still pending when the program entry point returns to the Profound.js framework. Promise-based APIs should always be called with await. This ensures the proper sequence of events, and also converts any Promise rejections into exceptions that can be caught and handled by the framework.
For example, to use Node’s Promise-based API to read a file:
const fs = require("fs/promises");
async function readFile() {
let data = await fs.readFile("data.txt", "utf8");
console.log(data);
}
The await keyword is only valid within an async function, so make sure to add async to the containing function definition.
To run multiple Promises in parallel, APIs like Promise.all() can be used, with an await coded on the output Promise:
APIs that use callbacks that accept the conventional (error, result) arguments should be wrapped in a Promise using Node’s util.promisify() utility. For example:
APIs that use callbacks that accept other arguments can be manually wrapped in a Promise. For example:
Handling Exceptions
As long the await keyword is used with each Promise, exceptions can be handled with try/catch. For example:
Any exceptions that are not caught in the application will be caught and handled by the Profound.js framework. For example, if the application is a Rich Display File program, then the framework will display an error screen.
If the await keyword is omitted, then Promise rejections are not converted into exceptions that can be caught and handled by the application or by Profound.js. In this case, the behavior depends on the Node.js setting for unhandled Promise rejections, which is discussed in the next section. Depending on this setting, unhandled Promise rejections can cause the server process to crash. For example:
Unhandled Promise Rejections
When a Promise rejection is not handled (which happens when an await is missing somewhere), the behavior depends on the Node.js version, the --unhandled-rejections command line argument, and on whether/not the unhandledRejection hook is registered. See here:
https://nodejs.org/docs/latest-v16.x/api/cli.html#--unhandled-rejectionsmode
And here:
https://nodejs.org/docs/latest-v16.x/api/process.html#process_event_unhandledrejection
Starting with Node.js 16, the default behavior is throw. On prior versions, the default behavior is similar to warn. The recommended setting for Profound.js is throw. When starting Profound.js on IBM i with STRTCPSVR, --unhandled-rejections is forced to throw for all Node.js versions, unless a different option is specified in the instance configuration using nodeArgs. See here.
Profound.js does not register an unhandledRejection hook. Customers may register their own hook, but this is not recommended.
Limitations of Module Transformation
Module transformation is based on analysis of the source code. The process is not perfect, and it can’t automatically add async/await in all scenarios.
For example, consider these programs:
app.js
myModule.js
In this case, the module myModule.js can’t be loaded/transformed while transforming app.js because the module name is given by a variable. So, then, module transformation will not know to add await to the call to myModule.getProducts(). The result may be confusing at first. Instead of the array of products, the program produces output like this:
This happens because myModule.getProducts() is loaded/transformed later, when the pjs.requireModule() call runs as the application is executed. myModule.getProducts() is transformed into an async function because it calls pjs.query(). This means that it returns a Promise. However, the coding in app() doesn’t call it with await, so the value passed to console.log() is the Promise itself.
Another effect of the missing await is that the app() function returns to the Profound.js framework before the Promise for the query resolves. The framework doesn’t know that the application has a Promise pending, and it disposes of the session. Eventually the Promise resolves and the myModule.getProducts() function resumes execution. It fails before with an error like this, because the session has been disposed of already:
This causes the Promise returned from myModule.getProducts() to be rejected, and the Profound.js Framework can’t catch and handle the exception normally. This can cause the server process to crash, depending on the Node.js setting for unhandled Promise rejections.
The solution is to adjust the app() function code and async/await where needed: