Reputation: 83
I'm a Node.js student, and I'm writing a brief example. The get on /org works as expected.
With the get on /orgg I tried to segregate the server "fetch the data" logic from the controller. However, the getRowsAsync() immediately returns a Promise.
This "put the await in express.get()" isn't good for hiding logic. If my biz logic needed some sequential Promises then the biz logic would have to bubble into the controller.
How can I do the equivalent to calling "doBizLogic()" in the controller, hiding my awaits, and having the controller wait for the logic to complete? Must I pass a callback function to the biz logic to make this scheme work?
Here is my index.js. I omit database.js, but I borrowed it from https://mhagemann.medium.com/create-a-mysql-database-middleware-with-node-js-8-and-async-await-6984a09d49f4
const express = require("express");
const urlPort = 3000;
let app = express();
let mysql = require("mysql2");
let pool = require("./database");
app.listen(urlPort, () => {
console.log("Server is running at port " + urlPort);
});
app.get("/", (req, res) => {
res.send("This app responds to /org and /org/:id only.");
});
app.get("/org", async (req, res) => {
let rows = await getRows("select * from org");
// the log always prints a JSON array.
console.log("in app.get for /org, rows: ", rows);
res.send(rows);
});
app.get("/orgg", (req, res) => {
let rows = getRowsAsync();
// the log() prints immediately prints a Promise.
console.log("in app.get for /orgg, rows: ", rows);
res.send(rows);
});
function getRows(sql, params = []) {
let rows = pool.query(sql, params);
// the log() prints a Promise.
console.log("in getRows, rows: ", rows); // returns
return rows;
}
async function getRowsAsync() {
let rows = await getRows("select * from org");
// the log() prints a JSON array, once it is finally called.
console.log("in getRowsAsync, rows: ", rows);
return rows;
}
Upvotes: 2
Views: 400
Reputation: 707308
Your specific questions
How can I do the equivalent to calling "doBizLogic()" in the controller, hiding my awaits, and having the controller wait for the logic to complete?
You can't hide that logic. Any API that "might" be asynchronous must be designed as an asynchronous API and the caller needs to know how to use the asynchronous API properly.
You cannot hide asynchronous-ness because there's no way in Javascript to make an asynchronous operation into a synchronous one. If any part of your operation is asynchronous, then it will need an asynchronous interface to communicate the result and completion (via a promise, a callback or an event).
Must I pass a callback function to the biz logic to make this scheme work?
You could use a callback, but I would suggest just returning a promise that the caller can use await
on as that's a more modern design than a callback and is somewhat easier to program with, particularly if there are other asynchronous operations in the same function. The caller will have to make use of that returned promise for things to work properly.
So, just have your caller use await
. The caller has to "know" it's an asynchronous interface anyway because you program with asynchronous interfaces differently than synchronous interfaces.
This "put the await in express.get()" isn't good for hiding logic. If my biz logic needed some sequential Promises then the biz logic would have to bubble into the controller.
Yes, that's the way it is in Javascript. A caller has to know if they are calling a synchronous or asynchronous API so they know how to get the result/err and completion. There is no way around that unless you design all interfaces as asynchronous and treat all calls to those APIs as asynchronous (which is not efficient for entirely synchronous APIs).
Why is await not being honored in a sub-function?
In this piece of code:
function getRows(sql, params = []) {
let rows = pool.query(sql, params);
// the log() prints a Promise.
console.log("in getRows, rows: ", rows); // returns
return rows;
}
The code is not properly handling the asynchronous pool.query()
. That function apparently returns a promise and you aren't waiting for that promise to resolve either with await
or .then()
. Therefore, when you try to log the value, you just get the promise. This function will be returning the promise so a caller can get the value by using .then()
or await
on that promise.
Await requires proper promise implementation
I will also mention that in order for await
to work properly for your callers, your function must return a promise that resolves or rejects when the asynchronous operation is complete and any resolved value or rejected error is in that promise also. So, just slapping an await
on a function that isn't returning a promise that is properly wired into the asynchronous operations will not work. The promise must be correctly implemented for await
to do its job.
Use a promise-supporting version of mysql
Since it appears you're using mysql, I'd strong suggest you get the module that implements a promise interface for the database so you can do all your asynchronous coding with promises, not plain callbacks.
The link you had in your question to a 2018 article shows lots of mysql examples using plain callbacks. I would not recommend starting a new project using plain callbacks for asynchronous operations. You will quickly learn to appreciate the advantages of using promise-based interfaces once you start doing that and promises are the modern way of handling asynchronous operations in Javascript.
You must catch all promise rejections
P.S. Any promise you use MUST have a .catch()
or an await
with a try/catch
somewhere in the call chain that will catch a potential rejected promise. So, all your express request handlers that are calling your asynchronous APIs are missing that proper error handling. Without that, you will get an uncaught promise rejection. At best, the request handler will just never send a response. At worst, your server will shut-down. All possible rejected promises must be caught somewhere in your code.
So, something like this:
app.get("/org", async (req, res) => {
let rows = await getRows("select * from org");
// the log always prints a JSON array.
console.log("in app.get for /org, rows: ", rows);
res.send(rows);
});
should be something like this:
app.get("/org", async (req, res) => {
try {
let rows = await getRows("select * from org");
// the log always prints a JSON array.
console.log("in app.get for /org, rows: ", rows);
res.send(rows);
} catch(e) {
console.log(e);
res.sendStatus(500);
}
});
Or, if you want to handle all errors centrally:
app.get("/org", async (req, res) => {
try {
let rows = await getRows("select * from org");
// the log always prints a JSON array.
console.log("in app.get for /org, rows: ", rows);
res.send(rows);
} catch(e) {
console.log(e);
next(e); // forward to central Express error handler
}
});
If you want to catch the error in your business logic so you can do something with it, but still eventually return an error back to your controller functions, then you can catch
and rethrow
:
async function getRows(params) {
try {
let rows = await pool.query("select * from org", params);
console.log("in getRows, rows: ", rows);
return rows;
} catch(e) {
// catch the error, log it and do whatever else you want with it
// either return a value or rethrow some error
// usually, you would throw some error here
// so a failed operation rejects the promise
// this function returns
console.log(e);
// you can throw a different error if you want to invent
// your own specific errors, rather than pass back db errors
throw e;
}
}
Upvotes: 4