santifer/career-ops · warning

⚠️ Browser cleanup failed: ${err.message}

Error message

⚠️  Browser cleanup failed: ${err.message}

What it means

Best-effort cleanup warning in generate-pdf.mjs's renderHtmlToPdf(): after renderInPage() finishes (or throws), browser.close() rejected and the rejection is only logged, never rethrown — a failed close must not mask a successful PDF render. Typical when Chromium already died (crash/OOM) or was closed twice.

Source

Thrown at generate-pdf.mjs:1448

 *   baseDir?: string,
 *   reportNum?: string,
 *   inputPath?: string,
 *   maxPages?: number,
 *   strictPages?: boolean,
 *   launchBrowser?: (options: {headless: boolean}) => Promise<import('playwright').Browser>
 * }} [opts]
 * @returns {Promise<{outputPath: string, pageCount: number, size: number}>}
 */
export async function renderHtmlToPdf(html, outputPath, opts = {}) {
  const launchBrowser = opts.launchBrowser || ((options) => chromium.launch(options));
  let browser = null;
  try {
    browser = await launchBrowser({ headless: true });
    return await renderInPage(browser, html, outputPath, opts);
  } finally {
    if (browser) {
      await browser.close().catch((err) => {
        console.warn(`⚠️  Browser cleanup failed: ${err.message}`);
      });
    }
  }
}

/**
 * Render one already-normalized HTML document to a PDF on an already-launched
 * browser. This is the page-level half of the render — it owns the per-document
 * work (theme/print/font injection, temp file, page, PDF, page-budget, manifest)
 * but NOT the browser lifecycle. Both the single-CV path (renderHtmlToPdf) and
 * the batch path (renderBatch) call this exact function, which is what keeps a
 * single-CV render byte-identical whether it runs alone or inside a batch (#2384).
 *
 * The page and the temp HTML file are always cleaned up in a finally, so a
 * throw here (e.g. a strict page-budget overflow) never leaks a page into the
 * shared browser — the caller's remaining documents keep their own fresh pages.
 *
 * @param {import('playwright').Browser} browser - An open browser to render on.

View on GitHub (pinned to 60398d6549)

Solutions

  1. Treat the warning as noise if the PDF was written — check the returned outputPath/pageCount
  2. For repeated occurrences in batches, reduce parallelism or raise memory so Chromium stops dying
  3. If injecting a custom launchBrowser, do not close the browser yourself; let this function own the lifecycle
  4. Confirm no double-close: renderHtmlToPdf owns close(); renderInPage callers must only close pages/contexts they created
Defensive patterns

Strategy: fallback

Validate before calling

// Before a batch, sanity-check that Chromium can launch at all
const browser = await (opts.launchBrowser || ((o) => chromium.launch(o)))({ headless: true });
await browser.close().catch(() => {}); // probe succeeded if we got here

Try / catch

// The function already swallows close() rejections; trust the return value:
try {
  const { outputPath, pageCount } = await renderHtmlToPdf(html, out, opts);
  // success — a cleanup warning afterwards is noise
} catch (err) {
  // real render failure; browser cleanup already handled best-effort
  throw err;
}

Prevention

When it happens

Trigger: Calling renderHtmlToPdf() where the launchBrowser() chromium instance crashed mid-render (OOM-killed headless process), or a test double/outer harness already closed the browser before the finally ran. The render result is unaffected unless the crash itself failed the render earlier.

Common situations: Memory pressure in batch/parallel CV rendering killing Chromium; custom launchBrowser injection in tests that closes eagerly; CI sandboxes with low memory limits.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/208d8ea19efd2630. Report an issue: GitHub.