Promises and async/await
Users upgrading from Profound.js 5 to Profound.js 6 should read the migration notes first.
Overview
The Profound.js framework is designed to allow developers to create business applications in Node.js, but without all the complexity of traditional Node.js programming – asynchronous I/O, Callback Hell, Promise chains, and so on. The framework takes care of all asynchronous calls behind the scenes, allowing applications to be coded in a simple top-down manner.
Before version 6, the Profound.js framework used the open-source Fibers add-on for Node.js to handle asynchronous calls in a top-down manner. Profound.js version 6 replaces Fibers with standard JavaScript Promises and async/await to accomplish the same result.
This page describes how the Profound.js framework handles Promises. For background on how Promises and async/await work in standard Node.js, and the pitfalls the framework is designed to avoid, see Coding With Promises in Node.js.
Table of Contents
- 1 How Profound.js Handles Promises
- 2 What Is Module Transformation?
- 3 How Does Module Transformation Help With Promises?
- 4 Using Asynchronous Node.js and Third-Party APIs in Profound.js
- 5 Handling Exceptions
- 6 Unhandled Promise Rejections
- 7 Limitations of Module Transformation
- 7.1 app.js
- 7.2 myModule.js
- 8 Related Pages
How Profound.js Handles Promises
All Profound.js framework APIs that perform asynchronous operations (such as reading files, running database queries, and so on) 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 described 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 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.awaitis added onto any Profound.js framework API calls, if not already coded.Calls to
require()andpjs.import()are detected, and the target module is loaded and transformed if possible. Calls to any importedasyncfunctions haveawaitadded if not already coded.If module transformation adds
awaitinside a function that appears to be used with an Array iteration method (for example,Array.forEach()), the Array iteration method is optionally replaced with a Promises-aware version that runs the callback for each array element in series.awaitis added on the call to the Array iteration method (see asyncArrayIteration).Any function that module transformation inserts an
awaitinto is changed toasync, if not already coded that way.Then, any calls to functions that are found to be
asyncas a result of the above haveawaitadded, if necessary, and the process repeats over the entire call tree.Optionally,
awaitcan be added to calls to allasyncfunctions in the module (see autoAsyncAwait).
Using Asynchronous Node.js and Third-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.
Calling a Promise-Based Node.js API
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.
Running Multiple Promises in Parallel
To run multiple Promises in parallel, APIs such as Promise.all() can be used, with an await coded on the output Promise:
const fs = require("fs/promises");
async function readFiles() {
let promises = [
fs.readFile("data1.txt", "utf8"),
fs.readFile("data2.txt", "utf8")
];
let data = await Promise.all(promises);
console.log(data);
}Wrapping Callback-Based APIs
APIs that use callbacks accepting the conventional (error, result) arguments should be wrapped in a Promise using Node’s util.promisify() utility. For example:
const fs = require("fs");
const util = require("util");
async function readFile() {
let readFile = util.promisify(fs.readFile).bind(fs);
let data = await readFile("data.txt", "utf8");
console.log(data);
}
APIs that use callbacks accepting other arguments can be manually wrapped in a Promise. For example:
await new Promise(resolve => setTimeout(resolve, 10000));Handling Exceptions
As long as the await keyword is used with each Promise, exceptions can be handled with try/catch. For example:
const fs = require("fs/promises");
try {
let data = await fs.readFile("data.txt", "utf8");
}
catch (error) {
// Any error, such as file not found, is caught here.
}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:
const fs = require("fs/promises");
try {
// An error on this call will be caught below.
let data1 = await fs.readFile("data1.txt", "utf8");
// An error on this call can't be caught!
let data2 = fs.readFile("data2.txt", "utf8");
}
catch (error) {
}
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 the unhandledRejection hook is registered. For more information, see the Node.js documentation for --unhandled-rejections and the unhandledRejection event.
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 Starting and Ending Profound.js Instances).
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 cannot automatically add async/await in all scenarios.
For example, consider these programs:
app.js
function app() {
let moduleName = "myModule.js";
let myModule = pjs.requireModule(moduleName);
let products = myModule.getProducts();
console.log(products);
}
module.exports.default = app;myModule.js
function getProducts() {
return pjs.query("select * from productsp");
}
exports.getProducts = getProducts;In this case, the module myModule.js cannot be loaded and transformed while transforming app.js, because the module name is given by a variable. Module transformation therefore does 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:
Promise {
<pending>,...This happens because myModule.getProducts() is loaded and 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() does not 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 does not 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 then fails with an error like this, because the session has already been disposed of:
TypeError: Cannot read property 'fields' of undefined
at Object.profound.closeFiles
at Object.profound.closeAll
at Object.pjs.<computed> [as closeAll]
at Object.getProducts (MyDir\modules\test-promises\myModule.js:4:26)This causes the Promise returned from myModule.getProducts() to be rejected, and the Profound.js framework cannot 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 add async/await where needed:
async function app() {
let moduleName = "myModule.js";
let myModule = pjs.requireModule(moduleName);
let products = await myModule.getProducts();
console.log(products);
}