Answer
Pug does not provide a built‑in cancellation mechanism, so using Promise.race for a timeout is acceptable only if you also detach or ignore the losing promise. Otherwise the render promise may resolve after the request/context has been cleaned up, causing Pug’s internal view lookup to fail with “Failed to lookup view …”.
Why the error occurs
When Promise.race([pug.renderFile(...), timeoutPromise]) resolves because the timeout promise rejects first, the pug.renderFile promise remains pending. If that pending promise later resolves, the rendering code tries to access the view paths after the HTTP response (or surrounding scope) has already been finalized, leading to the lookup error.
Confirmed facts
pug.renderFile returns a promise that resolves with the rendered HTML string.
- If that promise is not awaited or is left unhandled, any eventual resolution can trigger side‑effects after the caller has moved on.
- Detaching the losing promise (e.g., attaching a no‑op
.catch) prevents unhandled‑rejection warnings and stops the stray resolution from accessing cleaned‑up resources.
Recommended fix
- Create a wrapper that races the render promise with a timeout and ensures the render promise is ignored on timeout:
function renderWithTimeout(template, data, ms) {
const render = pug.renderFile(template, data);
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Render timeout')), ms)
);
return Promise.race([render, timeout]).then(
value => value,
err => {
// Detach the render promise so its eventual resolution does nothing
render.catch(() => {});
throw err;
}
);
}
- Use the wrapper with
await (or .then/.catch) and handle the rejected timeout as you would any other error:
try {
const html = await renderWithTimeout('views/page.pug', locals, 5000);
res.send(html);
} catch (e) {
if (e.message === 'Render timeout') {
res.status(504).send('Template rendering took too long');
} else {
// genuine Pug errors (e.g., "Failed to lookup view …")
res.status(500).send(e.message);
}
}
How to distinguish a timeout from a lookup failure
The wrapper above rejects with a custom error whose message you control (e.g., "Render timeout"). Any other error originating from Pug—including view‑lookup failures—will bubble up unchanged, allowing you to treat them separately in your catch block.
If you continue to see the "Failed to lookup view" error after applying this pattern, please share your Pug version and the full stack trace so we can verify whether a version‑specific behavior is involved.