Pug 3.x Promise returns vs Express.js callback-based res.render integration
0 reputation · 25 Feb 2023, 13:29 UTC
0 reputation · 25 Feb 2023, 13:29 UTC
When integrating Pug 3.x as an Express.js view engine, the template compiler emits async functions whenever a template uses await in iteration, async mixins, or async includes. The compiled render function then returns a Promise<string> instead of a plain string. Express's res.render method, however, expects the view engine's __express callback to invoke its callback with (err, html) synchronously or via callback, not to return a Promise. The built-in pug.__express adapter wraps renderFile but does not automatically await async template functions before calling back, creating a contract mismatch that surfaces as unhandled promise rejections or empty responses when templates contain async constructs.
The uncertainty centers on whether the adapter can be configured to detect and await async compiled functions transparently, or whether every async template requires a custom wrapper middleware that explicitly awaits pug.renderFile before invoking res.send. Third-party middleware that still passes callbacks to the view engine further complicates the upgrade path from Pug 2.x.
pug.__express in Pug 3.x internally await the compiled template function when it returns a Promise, or must application code supply a custom view engine wrapper?32275 reputation · 25 Feb 2023, 18:33 UTC
The built‑in pug.__express adapter in Pug 3.x does not await the Promise returned by a compiled template that uses await, async mixins, or async includes. It simply passes the Promise to the callback, which Express expects to receive a string. As a result, rendering such templates with res.render produces unhandled rejections or empty responses.
To use async templates, wrap pug.renderFile (or pug.__express) in your own middleware that checks the return value. If it’s a Promise, await it and then call the callback with the resolved string. This wrapper can be applied per‑route or globally, leaving legacy synchronous templates untouched.
await return Promise<string>.res.render calls the view engine’s __express callback synchronously and expects a string or an error.const pug = require('pug');
const original = pug.__express;
function asyncAdapter(filePath, options, callback) {
try {
const result = original(filePath, options, callback);
if (result && typeof result.then === 'function') {
// The compiled template returned a Promise.
result
.then(html => callback(null, html))
.catch(err => callback(err));
}
// If result is a string, the original callback already handled it.
} catch (err) {
callback(err);
}
}
module.exports = asyncAdapter;
Use this adapter in Express:
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
app.engine('pug', asyncAdapter); // replace the default
pug.__express for legacy routes that never use await.await pug.renderFile(...) in the route handler and then res.send.Async mixins force the whole template to be async. If you need to support both sync and async rendering in the same codebase, either:
node_modules/pug/lib/index.js to confirm the adapter does not await the result.res.render and observe that the response is empty.Does your code sometimes call pug.renderFile directly (outside of res.render)? If so, you’ll need to wrap that call as well to avoid unhandled rejections.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.