Failed to lookup view error in pug.renderFile when using Promise.race for timeout
0 reputation · 20 Dec 2023, 11:04 UTC
0 reputation · 20 Dec 2023, 11:04 UTC
Goal: abort a long‑running pug.renderFile call after a configurable interval without waiting for the template to finish rendering.
Constraint: pug.renderFile returns a Promise but does not inspect AbortSignal or other cancellation tokens, so a timeout created with Promise.race only rejects the outer Promise while the internal rendering task may continue to completion, wasting CPU.
Uncertainty: whether the current approach of wrapping the Promise with a timeout is adequate, or if Pug should provide a built‑in mechanism to halt parsing early, and how to differentiate a timeout rejection from genuine errors such as “Failed to lookup view …”.
Questions: Should Pug accept an AbortSignal to cancel parsing early? Is it acceptable to rely on Promise.race for timeout handling given the possible wasted work? How can an application reliably distinguish a timeout‑induced rejection from a lookup‑failure rejection?
32775 reputation · 20 Dec 2023, 12:21 UTC
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 …”.
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.
pug.renderFile returns a promise that resolves with the rendered HTML string..catch) prevents unhandled‑rejection warnings and stops the stray resolution from accessing cleaned‑up resources.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;
}
);
}
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);
}
}
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.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 20 Dec 2023, 14:59 UTC
The error surfaced in your handler during a Promise.race timeout is not always the error that eventually settles the render promise. If the timeout wins, your catch block receives the timeout error, but the original render promise may still reject later with Failed to lookup view. Inspecting only the caught error can misclassify a timeout as a missing template.
A practical fix is to reject the timeout with a distinct error type—for example, a custom TimeoutError—so your application can branch on err instanceof TimeoutError rather than parsing message strings. The detached render promise still needs a .catch(() => {}) to absorb the late lookup rejection, but that rejection never reaches your timeout handler.
Note that Pug template compilation and execution are synchronous CPU-bound operations. Even if Pug accepted an AbortSignal, it would likely only cancel the initial file I/O, not interrupt a long-running render. If your timeout guards against CPU exhaustion from complex templates, Promise.race mitigates the caller's wait, not the underlying CPU cost.
Pug's promise behavior varies across major versions, so verify against your exact version.